mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 14:49:09 +02:00
v0.4.0
486 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
45ff984518 |
fix(labels): sweep resource-label junctions on bulk deletes (MUL-4479) (#5348)
* fix(labels): sweep resource-label junctions on bulk deletes (MUL-4479) Runtime, runtime-profile, and workspace deletion hard-delete their agents and skills without clearing agent_to_label / skill_to_label. Migration 173 dropped the junction foreign keys, so these rows are no longer cascade-cleaned; once resource labels are enabled, every labelled agent/skill removed through one of these bulk paths leaves a permanent, invisible orphan junction row. Clear the junctions in the same transaction as the owner delete, before the owning rows disappear: - DeleteAgentRuntime / ArchiveAgentsAndDeleteRuntime / DeleteRuntimeProfile: DeleteAgentLabelAssignmentsByRuntime, ahead of the archived-agent hard-delete. - DeleteWorkspace: DeleteAgentLabelAssignmentsByWorkspace + DeleteSkillLabelAssignmentsByWorkspace, ahead of the workspace cascade. Adds regression tests for all four delete paths. Follow-up to #5345 (Elon review). Resource labels must stay disabled until this lands. Co-authored-by: multica-agent <github@multica.ai> * fix(labels): make workspace cleanup atomic Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Eve <eve@multica-ai.local> |
||
|
|
411a160b99 |
fix(release): harden v0.3.44 migrations (#5345)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
41b3045efa |
MUL-4424: bound Codex app-server startup RPCs (#5319)
* fix(codex): bound app-server startup RPCs Co-authored-by: multica-agent <github@multica.ai> * test(codex): de-flake bounded-handshake test The single 500ms handshake bound was shared by the successful preamble RPCs, so a slow fork/exec of the /bin/sh fake app-server could make initialize spuriously time out under parallel load. Raise the test bound to 3s (still below the 5s semantic timeout and 10s harness ceiling) and loosen the elapsed assertion to match. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: J <j@multica.ai> |
||
|
|
220fa58264 |
fix: guide SSH installs to token login (#5318)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9a071b827f |
Revert "fix(chat): correct unread — archived sessions + mount-race auto-read …" (#5332)
This reverts commit
|
||
|
|
79c4589f7d |
fix(server): don't cancel active tasks when issue status → cancelled (#5328)
Moving an issue to `cancelled` used to auto-cancel every in-flight agent task on that issue. Users have no expectation that clicking "cancel" stops running agent runs, so this implicit coupling is removed from UpdateIssue and BatchUpdateIssues. Deleting an issue still cancels its tasks (the owning row disappears); a plain status change never does. Reassignment already didn't cancel tasks (#4963 / MUL-4113), so this makes status-cancel consistent. - Status-table-driven regression tests cover every active state the cancel query sweeps (queued / dispatched / running / waiting_local_directory / deferred) on both the single and batch paths. - Updated the multica-working-on-issues skill (SKILL.md + source map) and corrected stale comments in task.go and agent.sql that described the removed coupling as current. MUL-4465 |
||
|
|
cec071dc06 |
fix(chat): correct unread — archived sessions + mount-race auto-read (MUL-4360) (#5315)
* feat(skills): search runtime local skills * feat(skills): highlight matched substrings in runtime local skill search Reuse the shared HighlightText (the same component the global search command uses) to highlight matched substrings in a result's name, provider, description, and path, so styling stays consistent across the app. Narrow the search to the fields the row actually renders and drop `key`, so every match maps to something visible. While a query is active, lift the description's 2-line clamp so a match past the first two lines stays on screen instead of being clipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): zero unread for archived chat sessions across all badges (MUL-4360) Archiving a chat session flips status but deliberately does not advance last_read_at, and ListAllChatSessionsByCreator counted unread unconditionally. So an archived session that had unread replies kept reporting has_unread=true / unread_count>0 — a stuck badge the user can never clear (archived sessions are read-only and hidden from history, so there is no mark-as-read entry). MUL-4372 fixed only the quick-chat FAB surface; the sidebar Chat tab badge and the chat-window "other unread" header still counted it. Fix at the source: derive unread_count = 0 for status='archived' rows in ListAllChatSessionsByCreator. Because has_unread is server-derived as unread_count > 0, and all surfaces (FAB, sidebar via countUnreadChatMessages, chat-window header, and mobile) read this one payload, every badge drops archived sessions with no per-surface filter. last_read_at is left untouched so unarchiving restores the true unread state. Installed desktop clients benefit without an app update. Also zero unread optimistically in the archive mutation so no badge counts a just-archived session in the frame before the refetch lands (FAB already filtered archived; this keeps sidebar/header consistent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): stop auto-mark-read from clearing a transiently-active session on mount (MUL-4360) The chat page persists `activeSessionId`, so on a bare `/chat` navigation it restores the last-open session as active for one frame before its URL→store effect (which runs AFTER useChatController's effects, since the hook is called first) clears it back to null. The auto-mark-read effect fired in that gap and marked the restored-but-never-opened session read — its unread badge vanished though the right pane still showed "select a chat" and the user never opened it. This is why the sidebar count dropped (e.g. 2 → 1) just by entering the tab. Defer the read by a tick and cancel it on cleanup: a session that is only momentarily active (restored on mount, then cleared) has its pending read cancelled when activeSessionId changes; only a session that stays active past the tick — a real select, deep link, or refresh — is marked read. A live-store re-check in the timer is a belt-and-suspenders guard. Adds the previously-missing auto-mark-read coverage: a stable-active session is read after the tick; a momentarily-active one is not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: coderbaozi <YHbaozi1988@163.com> Co-authored-by: abun <103836393+coderbaozi@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bf288349f6 |
feat(project): add start_date and due_date fields (MUL-4388) (#5313)
Projects become schedulable planning objects alongside their issues: add optional start_date / due_date, mirroring issue.start_date / issue.due_date. This is only the first slice of #5227 — labels, metadata, and the editable metadata UI are still out of scope. - migration 166: two nullable DATE columns on `project` (calendar days, no FK/index — matches the issue end-state after migration 112) - sqlc CreateProject / UpdateProject carry the dates; UpdateProject uses narg so an explicit null clears - handler: parse YYYY-MM-DD (400 on bad format), rawFields-presence clear on update, and the hand-scanned SearchProjects query returns the columns - CLI: `project create/update --start-date/--due-date` (empty clears on update) - frontend + mobile types/zod schemas: the two new schema fields are nullable().default(null) so a project from an older backend (frontend deploys before backend) parses to null instead of degrading the batch to the empty fallback; added a search schema drift test - projects skill / CLI docs Part of #5227 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5f767d671a |
fix(redact): cover credential formats that leaked unredacted (#5274)
The GitHub-token rule only matched classic tokens (ghp_/gho_/ghu_/ghs_/ghr_), even though its comment claims to cover fine-grained tokens. Add coverage for GitHub fine-grained PATs (github_pat_), Slack app-level (xapp-) / config (xoxe-), Google API keys (AIza...), and Stripe live secret/restricted keys (sk_live_/rk_live_). Publishable Stripe keys (pk_live_) are intentionally NOT redacted (public). Adds regression tests for every new shape, including a positive test that pk_live_ stays unredacted. |
||
|
|
a19e60a9e6 |
feat(chat): support images/files in agent chat replies (MUL-4287) (#5164)
* feat(chat): support images/files in agent chat replies (MUL-4287) Agents can now attach images/files to their chat replies, matching how comment attachments already work. The write-side gap was that the assistant chat_message is synthesized server-side from the completion callback's text output and never bound any attachments. Backend: - migration 150: nullable attachment.task_id (+ partial index), the transient handle that ties an agent's in-run upload to the reply it produces. - POST /api/upload-file accepts task_id: gated to the task's own agent, in this workspace, on a chat task; tags the row with task_id + chat_session_id. - CompleteTask (chat branch) binds the task's still-unclaimed attachments to the assistant message via BindChatAttachmentsToMessage (rejects rows already owned by an issue/comment/chat_message). An empty-output reply that produced files still creates a message so the images have an owner. FailTask binds nothing. CLI: - `multica attachment upload <path>` uploads a file for the current chat task (task from MULTICA_TASK_ID or --task) and prints id / markdown_url / a ready-to-paste markdown snippet. Prompt: - web/mobile chat prompt tells the agent how to attach a file to its reply. Mobile: - chat:done handler now always invalidates the messages list so attachments (absent from the event payload) refetch; mirrors web's self-heal. - chat bubbles render standalone attachment cards via the existing CommentAttachmentList (dedup vs inline references), matching web. Web/desktop needed no change — they already render message.attachments inline and via AttachmentList, and self-heal on chat:done. Tests: upload permission/isolation, bind-on-complete, empty-output+attachments, FailTask no-bind, null task_id untouched, already-owned not stolen, CLI output contract, mobile refetch-on-done. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address review blockers on chat reply attachments (MUL-4287) Two final-review blockers on PR #5164: 1. Mobile inline dedup only checked raw `url`, so an attachment referenced inline via `markdown_url` (exactly what the CLI snippet emits) rendered twice — once inline, once as a standalone card. Reuse the core `contentReferencesAttachment` helper so dedup covers every real reference form (stable /api/attachments/<id>/download path, url, download_url, markdown_url), matching web's AttachmentList. Extracted the filter into a pure `lib/attachment-dedup.ts` so it is unit-testable, and added a regression test covering `content` containing `attachment.markdown_url` (plus the other URL forms and same-identity sibling dedup). 2. CLI `attachment upload` emitted `![...]` image markdown for every file, producing a broken-image snippet for non-images. Emit image markdown only for image/* content types and a plain link otherwise, with a CLI contract test for both. Approved scope otherwise unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): renumber attachment_task_id migration 150 -> 157 after main merge (MUL-4287) Merged latest main; main renumbered its migrations and now occupies 150-156, so 150_attachment_task_id collided with 150_agent_task_coalesced_comments and would fail TestMigrationNumericPrefixesStayUniqueAfterLegacySet. Renamed to the next unique prefix (157). No content change; migrate up applies cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): render agent-produced files as attachment cards, not raw links The chat upload command handed the agent a bare `[name](url)` markdown snippet. Pasted mid-sentence it renders as a plain text link (not a card), and the referenced URL hides the auto-bound standalone attachment — so a file the agent produced could end up showing as nothing. Return the block-level `!file[name](url)` card syntax instead (images keep `` inline), and markdown-escape the filename so names with `[`/`]` don't truncate the label. The prompt and CLI help now state the file auto-attaches below the reply and the snippet is optional, only for placement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): soften message-list scroll fade (32px → 16px) The 32px edge fade washed out full-bleed content (HTML / image previews) at the list edges. Halve the fade distance so it barely grazes previews while still hinting at more content above/below. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): renumber attachment_task_id migration 157 -> 158 main landed 157_agent_task_delivered_comments while this branch was open, colliding on prefix 157 and failing TestMigrationNumericPrefixesStayUniqueAfterLegacySet. Bump this PR's migration to the next free prefix (158). Rename only; the migration body (nullable attachment.task_id + partial index) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): pin attachment upload to the token's task; build index concurrently Two code-review findings on the chat-attachment path (MUL-4287): - Isolation/privacy: POST /api/upload-file only checked the form task_id belonged to the caller's agent, not that it matched the task-scoped token's authoritative X-Task-ID. A run authorized for task A could tag an attachment onto task B (another chat task of the same agent, possibly another user's session), binding it into that reply on completion. Require the form task_id to equal the server-set X-Task-ID; add a same-agent/other-task 403 regression. - Migration: split the task_id lookup index into its own migration (159) built with CREATE INDEX CONCURRENTLY (repo convention) — it cannot share a multi-command file with the ADD COLUMN in 158. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): enforce task-token source on attachment upload; drop transient task_id FK (MUL-4287) Addresses the two remaining Preflight BLOCKERs on PR #5164. Security (file.go): the task_id upload path compared the form task_id to X-Task-ID but did not require X-Actor-Source=task_token. A normal JWT/mul_ PAT leaves that header empty and the middleware does NOT strip a client-forged X-Task-ID; resolveActor's fallback accepts a valid X-Agent-ID+X-Task-ID pair. So a member who learned a task ID could forge both and inject an attachment onto another chat task's assistant reply (cross-session/privacy leak). Now the branch requires X-Actor-Source=task_token first (mirrors chat_history.go's load-bearing boundary), then pins to the middleware-injected X-Task-ID. Tests now go through the real task-token headers and add a forged-JWT-403 regression. Migration (158): task_id is a transient binding handle (written once at upload against an already-validated task, read only during that task's own completion; durable owner is chat_message_id). There is no app-layer path that hard-deletes agent_task_queue rows, and orphan uploads are already reaped by attachment.chat_session_id's ON DELETE CASCADE — so an FK here would only add a cascade dependency the app never relies on plus write overhead on the hot attachment table. Drop the FK; task_id is now a plain UUID column. Added a regression test that an unbound task-tagged upload is reaped on chat_session delete. Index (159, CONCURRENTLY) unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(mobile): align !file card preprocess with web parser + CLI escaped labels (MUL-4287) Howard final-review blocker: mobile's `!file[...]` preprocess didn't keep up with the CLI's file-card output, so agent-produced non-image files rendered nowhere on mobile. - `FILE_LINE_RE` used `[^\]]+` for the label, so the CLI's escaped-bracket output `!file[a\]b.pdf](url)` (cmd_attachment.go escapeMarkdownLabel) never matched — the line stayed literal AND `standaloneAttachments` still hid the fallback card (the URL is in `content`), so the file showed nowhere. - Align the matcher with web's `packages/ui/markdown/file-cards.ts`: label allows backslash-escaped metacharacters (ReDoS-safe class), and the URL is restricted to the same allowlist (site-relative /uploads + /api/attachments/ <UUID>/download, plus absolute http(s)); disallowed schemes stay plain text. - Unescape the label to the real filename, then re-escape only the chars that would break a markdown LINK label (mobile emits `[📎 name](url)`, re-parsed by the renderer — unlike web's HTML data-filename), so a raw `]` never truncates the link text. No dedup change: once the inline `!file` renders, hiding the standalone card is correct. Added focused unit tests covering the escaped-label case, parens/ backslash unescape, the site-relative URL form, and disallowed-scheme rejection. 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> |
||
|
|
1427e8abd3 | feat(agents): add conversational creation studio (#5296) | ||
|
|
7356b56a10 |
fix(taskfailure): anchor HTTP status-code matches to digit boundaries (#5275)
Anchor the 401/402/403/429/529 HTTP status-code matches to digit boundaries so embedded numbers (e.g. "402913 tokens", "15290ms") no longer misclassify process/unknown failures as provider errors and skew failure observability. Mirrors the existing 5xx anchoring (providerHTTP5xxRe). Fixes #5271 MUL-4422 |
||
|
|
c377d7fb4f |
feat(labels): add scoped label management (#5279)
* feat(labels): add scoped label management * fix(labels): address review feedback * fix(migrations): use unique label migration prefix |
||
|
|
a14098288b | feat: redesign agent Skills and MCP capabilities (#5277) | ||
|
|
53f05cca5e |
feat(github): fan out PR/check_suite webhooks to all bound workspaces (MUL-4343) (#5218)
* feat(github): fan out PR/check_suite webhooks to all bound workspaces (MUL-4343) One GitHub App installation can be bound to several workspaces (#4855), but pull_request and check_suite webhooks were still routed to a single workspace via resolveWorkspaceForRepo (workspace.repos registry + oldest-binding fallback). Every workspace but one silently received nothing for a shared repo, with no way to opt in. Deliver each repo event to every workspace bound to the installation. Repo scope is whatever GitHub authorized the installation for; we no longer gate on the workspace.repos registry (that list means "code the agent clones", not a webhook subscription). Each workspace independently mirrors the PR, auto-links against its own issue prefix + github toggles, records check suites against its own PR mirror, and gets its own realtime broadcast. - Extract mirrorPullRequestForWorkspace / recordCheckSuiteForWorkspace and loop over all installation bindings instead of resolving one workspace. - Remove the now-dead resolveWorkspaceForRepo / repoIdentityFromURL routing. - Replace the registry-routing tests with PR + check_suite fan-out tests. Co-authored-by: multica-agent <github@multica.ai> * test(github): out-of-order fan-out test + drop dead query + document multi-workspace delivery (MUL-4343) Addresses review feedback on the webhook fan-out change: - Add TestWebhook_CheckSuite_OutOfOrderFansOutToBoundWorkspaces: a check_suite that arrives before the PR must stash a pending row per bound workspace, and each workspace must drain its own row when the PR fans out. - Remove the now-unused ListWorkspacesWithRepos query (its only caller was the deleted resolveWorkspaceForRepo) and regenerate sqlc; fix the stale "picks the target workspace via the repos registry" comment on ListGitHubInstallationsByInstallationID. - Document multi-workspace event delivery in the GitHub integration docs (en + zh), including an explicit self-host upgrade note: delivery is now keyed on the GitHub connection, so a workspace that relied on the code-repository list alone (without connecting GitHub) must connect the installation to keep receiving events. This is an intentional, documented behavior change — the PR description's earlier "single-binding behavior is unchanged" claim was inaccurate and has been corrected. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cb87dd106b |
feat(chat): task-owned direct-chat input batches + explicit no_response outcome (MUL-4351) (#5195)
* feat(chat): task-owned direct-chat input batches + explicit no_response outcome (MUL-4351) Direct (web/mobile) chat no longer uses the last-assistant-row as an implicit input cursor. Each direct send now owns an immutable input batch: - agent_task_queue.chat_input_task_id makes a task the owner of the user messages it must consume; the send path creates the task + user message + attachment bindings + session touch in one transaction, and the daemon is notified only after commit. A claim reads exactly that batch, so a message that arrives mid-run belongs to the next task and is never absorbed. - Auto-retry inherits the root input owner and is queued at a bumped priority, created inside FailTask's transaction so no newer chat task can jump ahead. - CompleteTask writes exactly one assistant outcome inside the completion transaction: a normal message, or a visible no_response outcome (with a non-empty English fallback) when the final output is empty. The write failing rolls the completion back and the handler returns 5xx so the daemon retries; the status CAS keeps it idempotent. chat:done carries message_kind. - Web/desktop/mobile render no_response as a localized 'no text reply' state (keeping the tool timeline), suppress Copy, keep it unread, and keep the session-list preview non-blank. - Legacy/channel tasks (chat_input_task_id NULL) keep the trailing-message selector, so a rolling deploy never replays Slack/Lark history. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): scope no_response to direct tasks; don't cancel task on input read error (MUL-4351) Addresses PR review (Niko): - writeChatCompletionOutcome only writes a no_response row for task-owned direct tasks (chat_input_task_id set). Legacy/channel (Slack/Lark) tasks keep the prior behavior: empty output writes no assistant row, so chat:done carries empty content and the channel outbound silently drops it — the no_response fallback body never reaches an external channel. - The daemon claim distinguishes a genuine zero-input batch from a failed input read: on ListChatInputMessages / ListChatMessages error it returns 5xx and preserves the dispatched task for redelivery instead of cancelling a valid task on a transient DB error. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4217de4389 |
fix(lark): tolerate binding token clock skew (#5191)
* fix(lark): tolerate binding token clock skew Clamp binding-token expiry against the database clock while preserving the 15-minute TTL cap. Return the persisted expiry so binding cards reflect the value enforced by Postgres. * docs(lark): correct stale table name in binding token TTL comments Post-#124 the table is channel_binding_token (with the channel_binding_token_ttl_cap CHECK); update the two comments in types.go and binding_token_test.go that still named the pre-generalization lark_binding_token table. --------- Co-authored-by: Bohan-J <bohan.optimism@gmail.com> |
||
|
|
f7ca045fb1 |
feat(daemon): discover Codex model and reasoning catalog dynamically (#5198)
Discover the Codex model list and per-model reasoning efforts from the installed CLI (codex debug models --bundled), with a verified static fallback for old/offline installs. Server gates token syntax; the daemon validates the exact (model, effort) pair. Closes #5197 MUL-4354 |
||
|
|
bf161f2f9c |
fix(tasks): preserve merged comment delivery (#5192)
Track actual claim-time delivery, support legacy daemons, and repair comment batches across claim, retry, edit, and delete races. MUL-4348 Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8e0fbecab5 |
fix(models): codex empty-model effort validation + exact 5.6 aliases (MUL-4347) (#5196)
Follow-up to #5188 addressing the second-round review. - ValidateThinkingLevel now fails an empty codex model closed instead of borrowing the flagged Default (gpt-5.6-sol). An empty model follows config.toml, which can resolve to any installed model; Sol alone advertises `ultra`, so the old borrow green-lit levels Luna / gpt-5.5 don't support and Codex doesn't reject. Checked before ListModels so a discovery error can't fail it open. Frontend pickModelEntry mirrors this (no per-model effort preview for an empty codex model); the persisted-orphan clear path stays. - parseCodexDebugModels drops efforts without a known label so the picker never advertises a level the Create/Update enum gate would 400 on save; the contract test now drives the real parser with an unknown effort instead of comparing two hand-written maps. - gpt-5.6 price aliases anchor to a literal dot (not the [.-] class), so dashed variants like gpt-5-6-luna surface as unmapped on both backend and frontend rather than silently borrowing a tier. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6b980a8e71 |
feat(models): add Codex gpt-5.6 series (sol/terra/luna) to model list & pricing (MUL-4347) (#5188)
* feat(models): add Codex gpt-5.6 series (sol/terra/luna) to model list & pricing (MUL-4347) Co-authored-by: multica-agent <github@multica.ai> * fix(models): official gpt-5.6 pricing, exact aliases, max/ultra effort levels (MUL-4347) - Replace provisional gpt-5.6 rates with OpenAI's official announcement values (sol 5/30, terra 2.5/15, luna 1/6); cache read 0.1x input, cache write 1.25x input (frontend + backend, kept in sync). - Anchor gpt-5.6 price aliases to exact match so unknown suffixed variants surface as unmapped instead of borrowing a tier. - Add Codex 0.144.1 max/ultra effort levels to the label map and server enum so the daemon-advertised catalog matches what the API can persist; add a catalog->API contract test. - Clarify that the codex Default flag is the effort-validation anchor, not a user-facing badge. - Note the cache-write measurement limitation (codex usage stream doesn't report cache-write tokens yet). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
521052a00b |
fix(daemon): recover stale Claude resume sessions (#5173)
Co-authored-by: CAVIN <zzz163519@users.noreply.github.com> |
||
|
|
6a72f248a1 |
fix: unblock release migrations (#5162)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
619b1b78e7 |
feat(server): remove generic LLM passthrough endpoints (MUL-4309) (#5154)
* feat(server): remove generic LLM passthrough endpoints (MUL-4309) Remove the OpenAI-compatible passthrough HTTP handlers LLMChatCompletions / LLMChatCompletionsStream and their two routes (/api/llm/v1/chat/completions[/stream]) plus their tests. Exposing a generic LLM proxy backed by the deployment key let any logged-in user run arbitrary completions on our dime. pkg/llm and the MULTICA_LLM_* config are kept unchanged as the server-internal LLM entry point, so chat title generation (maybeGenerateChatTitleAsync -> h.LLM.GenerateText) continues to work untouched. Updated the handler.go and .env.example comments to reflect internal-only usage. Co-authored-by: multica-agent <github@multica.ai> * docs(server): fix stale comments referencing removed LLM passthrough handlers (MUL-4309) Address GPT-Boy review nits: three doc comments still described the deleted OpenAI-compatible HTTP proxy handlers / 503 behavior. Update pkg/llm/client.go (package doc + ErrNotConfigured) and the Handler.LLM field comment to describe the internal-only usage. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
756e7e39b3 |
fix(chat): prune orphaned outbound card messages on chat-session delete (#4810) (#5152)
The standalone chat-session delete path pruned channel_chat_session_binding but not channel_outbound_card_message. Both are keyed by chat_session_id with no FK (MUL-3515 §4) and no reaper, so deleting a chat session left the card rows as permanent orphans — the same no-FK-orphan class as the #4810 installation fix, which already covers the workspace-delete / runtime-teardown / reclaim paths. Add DeleteChannelOutboundCardMessagesBySession and call it in the same tx as the binding prune; extend the delete-chat-session test to assert both are swept. Follow-up nit from the #5103 review (Elon). MUL-3937 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ccacce60a1 |
fix(channels): auto-reclaim orphaned IM-bot installations + accurate rebind conflict copy (#4810) MUL-3937 (#5103)
* fix(channels): auto-reclaim orphaned bot installations + accurate rebind conflict copy channel_installation has no FK to workspace/agent (MUL-3515 §4), so deleting a workspace or hard-deleting an agent left the row behind, occupying the (channel_type, app_id) routing slot forever — the bot could never be rebound and the UI had no way to clear it (#4810). The 409 also always blamed "a different Multica workspace" even when the real owner sat in the same workspace. Auto-reclaim on delete: - DeleteWorkspace and the runtime-teardown paths now sweep the workspace's / archived agents' channel installations and every dependent row in-tx. - The shared install path (Feishu + Slack) reclaims a DEAD prior owner — a revoked placeholder or an orphan whose workspace/agent is gone — before the upsert, healing installations stranded before this fix. A live owner (active agent, including an archived one) is left in place, not stolen. Accurate conflict copy: - A rebind refused by a LIVE owner now distinguishes same-workspace / another agent, an archived agent, and a genuinely different workspace, for both Slack (typed sentinels) and Feishu (registration message). MUL-3937 Co-authored-by: multica-agent <github@multica.ai> * fix(channels): reclaim cross-workspace revoked bots + sweep card/dedup/audit (#4810) Address the #5103 review (yyclaw + Steve): - Reclaim: a REVOKED installation in ANY workspace is now dead (except the caller's own row), not just same-workspace. Disconnect never hard-deletes the row and there is no release UI, so a cross-workspace revoked row would pin a bot's app_id slot forever, with the misleading "connected to another workspace" copy resurfacing. A new binder proves control by holding the app credentials, so reclaiming is safe. Live ACTIVE owners (incl. archived) are still refused. - Sweep the two dependent tables the cleanups missed, in all three paths (reclaim / DeleteWorkspace / runtime teardown): channel_outbound_card_message (no reaper, so a permanent orphan otherwise) and channel_inbound_message_dedup (PurgeChannelInboundDedup has no caller). - Audit rows: PURGE on the hard-delete paths instead of detaching them into permanently unattributable NULL rows; keep DETACH on reclaim, where the workspace survives and the row stays useful for triage. - Tests: flip cross-ws revoked to reclaimed + add cross-ws active preserved; extend the reclaim and both delete-path cleanup tests for card/dedup and the audit purge/detach split; assert the channel sweep on the DeleteRuntimeProfile entry point. MUL-3937 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4db1abe11d |
fix(comment): compensate dropped agent→agent @mentions in completion reconcile (MUL-4304) (#5148)
* fix(comment): compensate dropped agent→agent @mentions in completion reconcile (MUL-4304) When agent A explicitly @mentions agent B while B already has a dispatched/running task, the create-time enqueue path can only fold the comment into a QUEUED task; on a merge miss it defers to completion reconcile. But reconcileCommentsOnCompletion listed only member comments (ListMemberCommentsForIssueSince, author_type='member'), so A's agent-authored mention was never replayed and B was silently never woken — the intermittent 'agent @ agent fails to trigger' bug. Broaden the reconcile query to member+agent comments and route each under its own author_type. For an agent author, computeCommentAgentTriggers only produces triggers for explicit @agent/@squad mentions (plus the narrow assigned-squad-leader fallback), and reconcile still keeps only triggers routing to the agent that just ran — so plain agent replies never qualify and no unrelated agent is re-woken. Agent originator is resolved from the comment's source task so canInvokeAgent authorizes A2A correctly. Adds two covering tests: an agent-authored @B mention earns exactly one B follow-up; a plain agent reply (no mention) earns none. Co-authored-by: multica-agent <github@multica.ai> * fix(comment): address MUL-4304 review — exercise real dispatched drop + explicit-mention-only reconcile Review must-fix 1: the regression test used a 'running' task, which does not reproduce the drop (running-only is not AlreadyPending, so it takes the normal fresh-enqueue path). Rewrite it to drive the ACTUAL failure: B has a DISPATCHED task, agent A's explicit @B mention goes through the real trigger path (triggerTasksForComment), assert it is dropped at creation (0 queued follow-up), then complete B's task and assert reconcile recovers exactly 1 follow-up. Correct the 'dispatched/running' wording in daemon.go and comment.sql to 'dispatched'. Review must-fix 2: agent-authored comments on a squad-assigned issue can route to the squad leader via routeAssignedSquadLeaderFallback (a non-mention route), so 'plain reply yields nothing' was not unconditionally true. Scope reconcile's agent-comment compensation to EXPLICIT @agent/@squad mentions only (keepExplicitMentionTriggers, Source in {mention_agent, mention_squad_leader}); the squad-leader/assignee fallback and all other conversational routing are intentionally not replayed. Add a squad-assigned plain-worker-reply test proving the leader gets no completion-driven follow-up (verified failing without the filter). Update doc comments accordingly. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e6e63e6a13 |
feat(chat): LLM-generated chat session titles with silent fallback (MUL-4295) (#5141)
* feat(chat): LLM-generated chat session titles with silent fallback (MUL-4295) Generate a concise, language-matched title for a chat session after the first user message, replacing the raw first-message-derived title. The work is best-effort and fully non-blocking: - Triggered on the first user message in SendChatMessage (detected via ChatSessionHasUserMessage before insert), run in a detached goroutine so it never delays the send or first response. - Reuses pkg/llm GenerateText on the configured default model (MULTICA_LLM_DEFAULT_MODEL, else gpt-4o-mini); no model from the client. - Self-hosted with no LLM key (h.LLM.Enabled()==false): silent no-op, the original title stands. Same on timeout / upstream error. - CAS write (UpdateChatSessionTitleIfCurrent) so a manual rename during generation is never clobbered and titling runs at most once. - Pushes chat:session_updated so the frontend refreshes in place. - sanitizeChatTitle strips quotes/brackets, 'Title:'/'标题:' prefixes, trailing punctuation, and caps at chatSessionTitleMaxLen. Tests cover all six cases: configured→semantic title, disabled→fallback, upstream error→fallback, manual rename→no clobber, empty output→fallback, idempotent second run, plus sanitize rules and the realtime push. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): panic-contain title goroutine + loop sanitizer to a fixed point (MUL-4295) Address PR #5141 review (张大彪 / multica-eve, Phase B): 1. The detached title-generation goroutine now has a defer recover() at the top of its body. It runs outside chi's Recoverer, so an unhandled panic in GenerateText / sanitize / the DB write / publish would crash the server process. Best-effort path: log and keep the original title. 2. sanitizeChatTitle now alternates prefix-stripping and wrapper-stripping in a loop until the string is stable, so a forbidden label hidden inside a wrapper ("Title: Fix login", 「标题:修复登录问题」) is fully cleaned regardless of nesting order. Added both cases to the sanitize test table. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): fold trailing-punctuation trim into sanitizer fixed-point loop (MUL-4295) Address PR #5141 follow-up review: the trailing-punctuation trim ran once AFTER the prefix/wrapper loop, so a trailing '.' / '。' left the closing wrapper unrecognized and the forbidden prefix untouched for inputs like "Title: Fix login". and 「标题:修复登录问题」。. Trailing trim now runs inside the same loop, so removing the trailing punctuation re-exposes the wrapper (and the prefix it hid) on the next pass. Added both cases to TestSanitizeChatTitle. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0c2e48ded2 |
refactor: retire FF_RUNTIME_BRIEF_SLIM, make slim runtime brief the only path (MUL-4297)
The runtime_brief_slim feature flag has burned in; the slim runtime brief is now the sole path. - execenv: buildMetaSkillContent / BuildCommentReplyInstructions delegate to the slim assembler unconditionally; delete the legacy verbose brief body and writeBackgroundTaskSafetyInstructions. - Remove the runtime_brief_slim flag and the daemon-bound flag delivery subsystem built solely for it: execenv flag wiring (runtime_config_flag.go, server_snapshot_provider.go), the featureflagdispatch package, the DaemonFeatureFlagSnapshot heartbeat protocol field, and the server/daemon wiring in router.go, handler, daemon.go, main.go, cmd_daemon.go. - Keep the generic server/pkg/featureflag engine (still used by composio_mcp_apps). - Update tests to slim-only expectations and docs/feature-flags.md. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
75695a2e40 |
fix(comments): guarantee at-least-once processing of user comments (MUL-4195) (#5068)
* fix(comments): guarantee at-least-once processing of user comments (MUL-4195) Consecutive comments on an issue were silently dropped: a new comment that arrived while the agent already had a queued/dispatched task was discarded by the HasPendingTaskForIssueAndAgent dedup, losing the user's follow-up instruction with no visible trace. Comments — unlike chat — are deliberate, addressed, persisted input and must never vanish. This makes comment handling at-least-once while keeping concurrency bounded to one run per (issue, agent): - Merge, don't drop (PR1): a comment landing while a not-yet-started task exists is folded into that task — the prior trigger becomes a coalesced comment and the new one becomes the trigger, so a single run still covers every deliberate comment. Falls back to a fresh enqueue if the pending task was claimed mid-flight, so nothing is lost in the race. - Completion reconciliation (PR2): on task completion, a member comment newer than the run's started_at schedules exactly one follow-up via the normal trigger pipeline. Loop-safe: member-authored only, capped by the existing per-(issue,agent) dedup, and terminating. - Visibility (PR3): coalesced_comment_ids is surfaced on the task API and in the run prompt so the covered comments are explicit. Migration 145 adds agent_task_queue.coalesced_comment_ids UUID[]. Tests: merge-not-drop preserves all three of a rapid burst and repoints the trigger to the newest; reconciliation query gates on member/since; e2e CompleteTask enqueues a follow-up for a mid-run member comment and does not for none. Co-authored-by: multica-agent <github@multica.ai> * fix(comments): address review — originator gate, agent-scoped reconcile, cross-thread coalesced prompt (MUL-4195) Resolves GPT-Boy's Request-changes review on PR #5068. Must-fix #1 — merge no longer inherits a stale originator/runtime context. MergeCommentIntoPendingTask now only folds a comment into a pending task whose originator_user_id IS NOT DISTINCT FROM the new comment's originator. runtime_mcp_overlay / runtime_connected_apps are a pure function of (originator, agent) and the agent is fixed, so a matching originator keeps the stored overlay/attribution valid; a differing originator (e.g. user B commenting on a task originated by user A) matches no row and the caller enqueues a fresh follow-up with B's own context instead of reusing A's. trigger_summary is refreshed to the new trigger comment. Must-fix #2 — completion reconcile no longer re-wakes unrelated agents. reconcileCommentsOnCompletion computes the latest member comment's triggers and keeps ONLY the agent that just completed, instead of fanning the comment out through the full pipeline. An @-mention of agent B during agent A's run is triggered once at creation time and is no longer replayed (double-run) when A completes. Should-fix #3 — coalesced-comment prompt no longer assumes a single thread. The claim response now carries each folded comment's thread id / author / created_at / content (CoalescedCommentData); the prompt embeds them directly so the agent addresses cross-thread folded comments without the wrong "they are in the triggering thread" hint. Old servers that ship only ids fall back to an issue-wide fetch, still without the same-thread assumption. Tests: TestMergeCommentIntoPendingTask_OriginatorGate (query gate), TestCompleteTask_DoesNotReTriggerOtherAgentMentionedDuringRun (reconcile scoping), TestBuildCommentPromptCoalescedCrossThread / IDsOnlyFallback (prompt). Existing MUL-4195 suites still pass. Co-authored-by: multica-agent <github@multica.ai> * fix(comments): close unique-index drop + dispatched-window race in comment coalescing (MUL-4195) Second-round review follow-up on PR #5068. Must-fix #1 — originator-mismatch no longer drops the comment. The previous originator gate returned ErrNoRows on a different originator and the caller fell through to a fresh enqueue, which collided with the idx_one_pending_task_per_issue_agent unique index (one queued/dispatched task per (issue, agent)) — silently dropping the second user's comment. Replaced the gate with recompute-on-merge: MergeCommentIntoPendingTask now re-stamps originator_user_id, runtime_mcp_overlay, runtime_connected_apps and trigger_summary to the new comment's originator. A different member's comment folds into the single coalescing run carrying the latest instruction's own identity/overlay (no cross-user capability bleed, no drop, no collision). Must-fix #2 — comment arriving in the claim→StartTask window is no longer lost. Merge now targets only PRE-CLAIM states ('queued','deferred'); a dispatched/running task is never a merge target, so a post-claim comment is never falsely stamped into coalesced_comment_ids as "delivered". Completion reconcile is re-anchored on dispatched_at (the moment the claim response is built) instead of started_at, and sweeps ALL undelivered member comments since that anchor — replaying each through the normal enqueue path so they coalesce into one bounded, agent-scoped follow-up run. This covers the dispatch→start window a started_at anchor missed. Enqueue path: on a merge miss the caller no longer blindly fresh-enqueues (which could collide with a dispatched sibling); it defers to the active task's completion reconcile via HasActiveTaskForIssueAndAgent, and only fresh-enqueues when no active task exists. Tests: rewrote the query test to TestMergeCommentIntoPendingTask_RecomputesOriginatorAndSkipsDispatched; added TestConsecutiveCommentsDifferentOriginatorsFullEnqueuePath (full handler enqueue path, two distinct originators) and TestCompleteTask_ReconcilesDispatchedWindowComment (claim→start window). All existing MUL-4195 handler/cmd-server/daemon/service suites still pass. Co-authored-by: multica-agent <github@multica.ai> * fix(comments): catch pre-dispatch merge-race comment in completion reconcile (MUL-4195) Third-round review follow-up on PR #5068. Race: a member comment is created while the task is still queued, but its merge loses the race to the daemon claiming the task (queued→dispatched). The merge then finds no pre-claim row (ErrNoRows), the enqueue path defers to reconcile — but the comment's created_at is BEFORE dispatched_at, so the dispatched_at-anchored reconcile skipped it and the comment vanished with no task coverage. Fix: anchor completion reconcile on the task's created_at (which always precedes dispatch) instead of a dispatch/start timestamp, and exclude the run's DELIVERED SET — trigger_comment_id ∪ coalesced_comment_ids. Because merges only ever touch pre-claim rows, that set is exactly what the claim response carried, so any member comment created since the task was made that is NOT in it was genuinely undelivered and earns a bounded follow-up. This catches the pre-dispatch merge-race comment and the dispatch→start comment, while never re-firing a comment that was delivered as a pre-claim coalesced entry. Test: TestCompleteTask_ReconcilesPreDispatchMergeRaceComment reproduces the race (comment created pre-dispatch, task dispatched before merge, plus a delivered coalesced comment) and asserts exactly one follow-up, triggered by the race comment, with the delivered coalesced comment excluded. Existing reconcile fixtures updated to set a realistic created_at (the production invariant that created_at is the earliest task timestamp). Co-authored-by: multica-agent <github@multica.ai> * fix(comments): merge only into the queued task, never a deferred fallback (MUL-4195) Fourth-round review follow-up on PR #5068. MergeCommentIntoPendingTask targeted status IN ('queued','deferred') ordered by created_at DESC. When a (issue, agent) pair had both an older queued task (the run about to be claimed) and a newer deferred assignee-fallback task, a new comment merged into the deferred row instead of the queued one — so the comment missed the imminent run and the deferred fallback could later promote into a duplicate/conflicting run. This merge is only ever reached when HasPendingTaskForIssueAndAgent matched a queued/dispatched task (it never inspects deferred), so the coalescing target must be the queued row. Restricted the merge target to status = 'queued' (the unique index guarantees at most one). Deferred fallbacks keep their own fire_at/promotion escalation lifecycle and are never a merge target. Test: TestMergeCommentIntoPendingTask_TargetsQueuedNotDeferred seeds an older queued task + a newer deferred fallback for the same (issue, agent), merges a new comment, and asserts it lands on the queued task (trigger repointed, old trigger coalesced) while the deferred fallback is left untouched. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
22a71bafe3 |
feat(server): add basic LLM API layer with OpenAI-compatible endpoints (#5138)
Integrate the official openai-go SDK (v3) as a thin, reusable LLM layer (pkg/llm) backing lightweight utility calls that do not need the agent runtime (chat titles, quick-create drafts, ...). Expose two user-authenticated, OpenAI-compatible chat-completions endpoints: - POST /api/llm/v1/chat/completions (JSON response) - POST /api/llm/v1/chat/completions/stream (SSE stream) Requests decode directly into the SDK's ChatCompletionNewParams and responses are relayed via RawJSON() for byte-exact OpenAI-format compatibility. Base URL and API key are configurable (MULTICA_LLM_*), and the model is taken from the request with a configurable default fallback (MULTICA_LLM_DEFAULT_MODEL, else gpt-4o-mini). When unconfigured the endpoints return 503. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a51ab4d551 |
feat(chat): Chat V2 — first-class IM-style Chat tab (MUL-4171) (#5076)
* feat(chat): Chat V2 — first-class IM-style Chat tab (MUL-4171) Replace the floating chat FAB/window with a first-class Chat tab under Inbox, laid out as an IM-style two-pane surface (thread list + conversation). Highlights: - New Chat page (packages/views/chat/chat-page.tsx) with URL-addressable session selection; web + desktop routing wired up. Removes the old chat-fab / chat-window / resize-handles / context-items paths. - IM thread list: agent avatar + last-message preview + IM timestamp, red unread *count* badge (read-cursor model), presence-gated typing vs waiting. Rename lives only in the conversation header ⋯ menu (not the list hover). - Per-session conversation header (rename / view agent / delete), agent-aware empty state (avatar + name + description + starter prompts), and a deterministic clean-title derivation from the first message. - Server: read-cursor unread model (migration 145) and per-user pinned agents (migration 146, dedicated chat_pinned_agent table + handler/queries). New-agent welcome chat auto-enqueues a real agent run (LLM intro, no static template). - Design: fade the global --border token; borderless list headers on Chat/Inbox, kept (faded) on the conversation header. Verified: pnpm typecheck (all packages), go build ./..., go vet, gofmt. Co-authored-by: multica-agent <github@multica.ai> * feat(chat): make new-agent welcome read as an agent-initiated intro (MUL-4230) The "meet your new agent" chat used to insert a fake user message ("👋 Hi! Please introduce yourself …") and have the agent reply to it, so the thread looked like the creator prompting the agent. Drop the persisted user message. Flag the auto-created session is_agent_intro (migration 147) and drive the intro run server-side: the daemon builds a proactive self-introduction prompt for such sessions (buildChatPrompt) instead of a "reply to their message" prompt. The intro stays LLM-generated; the thread now opens with the agent's own message, as if it reached out first. - migration 147: chat_session.is_agent_intro - CreateChatSession carries the flag; sendAgentWelcomeChat no longer persists/publishes a user message - daemon: ChatIntro threaded from session flag → intro prompt Co-authored-by: multica-agent <github@multica.ai> * feat(chat): Settings toggle for the floating chat window (MUL-4235) (#5080) * feat(chat): Settings toggle for the floating chat window (MUL-4235) Re-introduce the floating chat overlay on top of Chat V2 as an optional, Settings-gated surface instead of deleting it outright. - Settings → Preferences → Chat: a switch (floatingChatEnabled, persisted client preference, default ON) to show/hide the floating window. - FloatingChat wrapper owns the two gates: the preference, and the /chat route (hidden on the tab so the same activeSessionId isn't shown twice). - ChatFab + a compact ChatWindow that reuse the shared useChatController and conversation components, so activeSessionId stays in lockstep with the tab. - Restore use-chat-context-items so the overlay's @ surfaces the current issue/project (the 'current context' affordance) — the tab stays manual. - i18n (en/zh-Hans/ja/ko), store unit tests. typecheck: core/views/web/desktop green. tests: chat store 9, settings 82, chat 39 pass. Co-authored-by: multica-agent <github@multica.ai> * feat(chat): dedicated Chat settings tab, floating window opt-in (MUL-4235) Address review: give Chat its own Settings tab instead of a section inside Preferences, and default the floating window OFF (opt-in). - New Settings → Chat tab (chat-tab.tsx) under My Account; moves the floating-window toggle out of the Preferences tab. - floatingChatEnabled now defaults OFF — only an explicit enable from the Chat tab mounts the FAB/overlay. - i18n: page.tabs.chat + a top-level chat block (en/zh-Hans/ja/ko); revert the Preferences chat section and its test mock; store tests updated for the opt-in default. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * refactor(chat): drop starter prompts from chat empty state (MUL-4237) (#5081) The three starter prompts (List my open tasks by priority / Summarize what I did today / Plan what to work on next) read as filler more than help, so remove them along with the now-unused returning_subtitle ("Try asking"). The empty state keeps its agent-aware header — avatar + "Chat with {name}" + optional description — and the composer stays the entry point. Locale keys dropped across en/zh-Hans/ja/ko (parity preserved). Based on the Chat V2 branch (parent MUL-4171, #5076), not main. Co-authored-by: Lambda <lambda@multica.ai> * feat(chat): pin a chat to the top of the Chat list (MUL-4240) (#5082) Builds on Chat V2 (#5076). Adds a per-conversation pin so a user can keep important chats at the top of the IM-style thread list, above the activity-sorted rest. Backend: - migration 148: chat_session.pinned_at (nullable) + partial index; the timestamp doubles as the pinned-group sort key and the boolean flag. - list queries order pinned-first, then by most-recent activity. - SetChatSessionPinned query + PATCH /api/chat/sessions/{id}/pin handler; pinning never bumps updated_at, so an unpinned chat won't jump the list. - ChatSessionResponse.pinned + chat:session_updated carries the new state. Frontend: - ChatSession.pinned; setChatSessionPinned API + useSetChatSessionPinned with optimistic re-sort; shared sortChatSessions comparator. - thread list: pin indicator on pinned rows + pin/unpin hover action; list sorted pinned-first so it stays ordered after cache patches. - realtime patch re-sorts on pin change; en/ja/ko/zh-Hans strings. Tests: SetChatSessionPinned handler test, sortChatSessions unit tests. * feat(chat): round send button, move file upload into a + menu (MUL-4250) (#5088) Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(inbox): match chat list selected-item style (inset padding + rounded) (#5093) Wrap the inbox list in p-1 and give each row rounded-md/px-3 so the selected bg-accent reads as an inset rounded card — same treatment the chat thread list already uses — instead of a full-bleed, sharp-cornered highlight. Content stays 16px-inset (p-1 + px-3 == old px-4). MUL-4253 Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): rename + menu upload item to "Image or files" (MUL-4250) (#5092) Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): stop welcome intro session repeating the same introduction (MUL-4259) The is_agent_intro flag on chat_session is persistent, so every follow-up turn on a welcome session re-selected the self-introduction prompt in buildChatPrompt and the agent kept replying with the same intro instead of answering the user. Gate resp.ChatIntro at claim time on the session still having zero human (role='user') messages via a new ChatSessionHasUserMessage query: the first, message-less server-driven turn introduces the agent; once the creator replies, later turns fall back to the normal reply prompt. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address review findings + unbreak CI (MUL-4171) - task:failed now refreshes the sessions list (invalidateSessionLists), so the thread-list preview / unread / sort stays correct after an agent failure — FailTask persists a failure chat_message but only broadcasts task:failed, mirroring the chat:done success path. - Self-heal stale chat deep links: once the sessions list has loaded and a ?session= id isn't in it (deleted / no access / never existed) with nothing in flight, clear the selection instead of rendering an editable empty chat that would POST into a nonexistent session. Freshly-created sessions are exempt (they carry optimistic messages + a pending task). - CI: add the new parameterless `chat` route to link-handler's WORKSPACE_ROUTE_SEGMENTS and to paths/consistency.test.ts (route set + expectedSegments) — keeps the two in sync, fixes the failing @multica/core test. - Fix a MUL-4235/MUL-4237 merge collision that broke @multica/views typecheck: chat-window.tsx still passed the removed `onPickPrompt` prop to EmptyState. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): self-heal dangling session in shared controller, not just ChatPage (MUL-4171) Re-review follow-up: the stale-session self-heal only lived in ChatPage, so the floating ChatWindow still entered from a persisted activeSessionId and would render an editable empty chat (then POST into a nonexistent session) when the selected session was deleted / lost access off the /chat route. - Move the self-heal into the shared useChatController so every surface (tab and floating window) drops a dangling activeSessionId once the sessions list has loaded and doesn't contain it. - Harden ensureSession: trust the current id only when it's in the loaded list or is a just-created session still awaiting the refetch; a dangling id falls through to create a fresh session instead of POSTing into a 404. - Exempt just-created sessions via an OPTIMISTIC-write signal (hasOptimisticInFlight: pending task or optimistic- message), not hasMessages — a session deleted elsewhere with real cached history stays eligible for self-heal. Add a unit test for the discriminator. Co-authored-by: multica-agent <github@multica.ai> * test(views): fix app-sidebar useWorkspacePaths mock for the new chat nav (MUL-4171) The AppSidebar personal nav gained a `chat` item, so it calls `useWorkspacePaths().chat()` at render. The app-sidebar.test.tsx mock hadn't been updated, so `p.chat` was undefined and every render threw `TypeError: p[item.key] is not a function`, failing @multica/views#test in CI. - Add `chat: () => "/acme/chat"` to the mocked useWorkspacePaths. - Route the chat-sessions query key through a mutable `chatSessions` fixture. - Add coverage for the Chat nav: renders the link, badges the summed unread_count, and hides the badge when all sessions are read — so this drift is caught next time. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): use the original floating window, not the rewritten one (MUL-4235) (#5102) Follow-up to the merged #5080, which shipped a hand-written, simplified ChatWindow and lost the original's animations / drag-resize / expand-minimize. The floating window is just a quick entry point — it should be the original UI, not a rewrite. - Restore chat-window.tsx, chat-fab.tsx, chat-resize-handles.tsx and use-chat-resize.ts verbatim from main (0-diff): motion animations, drag resize, expand/minimize and the session dropdown are back. - Restore the empty_state.returning_subtitle + starter_prompts i18n keys the original window renders (V2 had dropped them); drop the now-unused window.open_full_tooltip key the rewrite added. - Settings gating is unchanged: FloatingChat still wraps the original FAB + window, gated by floatingChatEnabled (default off) and hidden on /chat. typecheck: core/views/web/desktop green. tests: chat + settings views 126 pass. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * feat(chat): archive chats from the list, delete only from Archived (MUL-4263) (#5098) Restore an archive flow as the reversible sibling of delete: - Chat list hover now offers Archive (not Delete); pin/stop unchanged. - A footer entry ('Archived · N') opens an Archived view listing archived chats; hard delete lives only there (hover -> unarchive + delete, with the existing inline confirm). - Conversation header ⋯ menu mirrors this: active chats archive, archived chats unarchive/delete. Backend: PATCH /api/chat/sessions/{id}/archive flips status active<->archived (SetChatSessionArchived), broadcasts status on chat:session_updated so other tabs re-sort into the right list. SendChatMessage already refuses archived sessions, so archived chats stay read-only until unarchived. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * feat(chat): handle archived agents in Chat V2 list & conversation (MUL-4265) (#5100) * feat(chat): handle archived agents in Chat V2 list & conversation (MUL-4265) Co-authored-by: multica-agent <github@multica.ai> * refactor(chat): drop chat-list archive marker, keep conversation read-only (MUL-4265) Co-authored-by: multica-agent <github@multica.ai> * feat(chat): apply archived-agent read-only to the floating chat window (MUL-4265) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address floating-window + archived-agent review blockers (MUL-4171) Re-review follow-up on the restored floating ChatWindow + archive flow: 1. Floating stale-session self-heal. The restored ChatWindow doesn't use the shared controller, so its ensureSession trusted any non-empty activeSessionId and there was no dangling-session cleanup — a deleted / no-access persisted session could send into a nonexistent session. Ported the same guard used for the tab: a self-heal effect that clears a dangling activeSessionId once the sessions list has loaded, and ensureSession only trusts an id that's in the list or has an in-flight optimistic write (hasOptimisticInFlight, reused from use-chat-controller). handleSend seeds the optimistic message + pending task before setActiveSession, so a freshly-created session is never mis-cleared. 2. Floating dropdown bypassed archive-first safety. Its active rows offered a hard-delete, letting the floating window destroy active chats and skip the "archive first, delete only from Archived" model. Active rows now ARCHIVE (reversible, one-click) like ChatThreadList; the floating window offers no hard-delete — unarchive/delete live only in the full Chat page's Archived view (reachable via expand). Removed the now-dead delete-confirm machinery. 3. Orphan user message on archived-agent send. SendChatMessage created the chat_message before EnqueueChatTask, which rejects an archived / runtime-less agent — a stale client would land a user message then get a 500, orphaning it. Added a preflight that checks the session agent's archived / runtime state and returns 409 before any mutation, plus a handler test asserting the send is rejected with no message persisted. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fd58e13bec |
feat(runtimes): custom runtime names + searchable machine-grouped picker (MUL-4217) (#5070)
* feat(runtimes): custom runtime names + searchable machine-grouped picker
MUL-4217. Runtime names were daemon-generated ("Claude (host)") and
uneditable, so picking one at agent-create time was painful once a
workspace had many machines.
Phase 1 — create-agent RuntimePicker: add a search box (>6 runtimes) and
group options by machine (Local/Remote/Cloud, online-first, current
machine first) reusing buildRuntimeMachines/filterRuntimeMachines. Rows
show the provider under a machine header instead of a flat repeated list.
Phase 2 — custom names: new nullable agent_runtime.custom_name column,
never written by the registration/heartbeat upsert so the daemon can't
clobber it; display is coalesce(custom_name, name) via runtimeDisplayName.
PATCH /api/runtimes/:id gains custom_name (+ apply_to_machine to name every
runtime sharing a daemon_id in one action, owner-scoped for non-admins).
Rename UI on the runtime detail page; `multica runtime rename` CLI command.
Verified: go build/vet, sqlc, handler tests (incl. new custom-name single
+ machine-fanout), 1650 views + 764 core TS tests, typecheck, locale parity.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): address review — persist machine name on new registrations, keep custom_name in register response
Elon's review on #5070 (MUL-4217):
1. Machine name looked "lost" when a new provider registered on an
already-named machine — the new row landed with custom_name=null and
broke sharedCustomName. Now a fresh runtime inherits the machine's shared
custom name at register time (ListDaemonCustomNames + sharedDaemonCustomName),
so the machine title stays stable as providers come and go.
2. DaemonRegister rebuilt the response row by hand and dropped custom_name,
so register returned custom_name:null — inconsistent with list/get/update.
Both branches now carry CustomName.
Also: tighten the updateRuntime patch type to custom_name?: string (drop the
misleading `| null`, since the server treats null as "unchanged", not "clear").
Tests: register response preserves custom_name; new runtime inherits machine name.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): inherit machine name for failed-profile registrations too
Elon's re-review of #5070 (MUL-4217): the machine-name inheritance added
last round only covered the normal req.Runtimes path. The req.FailedProfiles
branch also upserts a daemon_id-scoped agent_runtime row (offline, profile
registration error), which shows up in the runtime list / machine grouping —
so on a named machine a failed custom-profile row landed with custom_name=NULL
and dragged the machine title back to the hostname.
Extract the inheritance into h.inheritMachineCustomName and call it from both
the normal runtime path and the failed-profile path. Add a test: named daemon
+ failed profile upsert -> the failed row's persisted custom_name is inherited.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
c4997af4d1 |
fix: archive autopilots on delete (#5042)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
33bd8aeaa9 |
MUL-4134: fix(lark): preserve same-agent bindings when reconnecting a revoked Feishu bot (#4997)
* fix(lark): allow rebinding a revoked Feishu bot to a different agent When a Feishu/Lark Bot is disconnected from agent A (status → revoked), the row is preserved for audit but still holds the (channel_type, config->>app_id) unique index slot. Binding the same Bot to agent B would fail with: duplicate key value violates unique constraint "idx_channel_installation_type_appid" (SQLSTATE 23505) because UpsertChannelInstallation conflicts on (workspace_id, agent_id, channel_type) — a different agent_id means no conflict match, so it tries INSERT and hits the app_id unique index. Fix: before the upsert, inside the same transaction, hard-delete any revoked installation with the same app_id in the same workspace. The delete is fenced to status=revoked so an active installation can never be silently removed. If no revoked row exists the delete is a no-op (deletes zero rows, returns nil error) and the upsert proceeds normally. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(lark): preserve same-agent bindings when reconnecting a revoked Feishu bot The cleanup added in the previous commit hard-deletes every revoked channel_installation sharing the app_id in the workspace before the upsert — including the row belonging to the agent currently being (re)installed. That regresses the common "disconnect then reconnect the same bot to the same agent" flow: disconnect only flips status to 'revoked' (bindings are preserved), and UpsertChannelInstallation conflicts on (workspace_id, agent_id, channel_type), so before this the same agent's row was reactivated in place — installation_id and every channel_user_binding / channel_chat_session_binding kept. Deleting it first forces an INSERT with a fresh installation_id, orphaning every member's account link (they must re-link) and all chat-session continuity; only the installer is re-bound. Fence the delete with `agent_id <> $agent_id` so it only clears a DIFFERENT agent's revoked row (the genuine app_id-slot blocker). The same agent's revoked row is left for the upsert to reactivate losslessly. Since idx_channel_installation_type_appid is globally unique on (channel_type, app_id), at most one row ever holds a given app_id, so the excluded row is exactly the one the upsert will reuse. Adds DB-backed regression tests: same-agent revoked row preserved, different-agent revoked row deleted, active row never deleted, other workspace fenced, plus end-to-end reactivation semantics (same agent keeps installation_id + bindings; different agent gets a fresh id). Co-authored-by: multica-agent <github@multica.ai> * fix(lark): clean dependent rows when hard-deleting a rebound Feishu installation Addresses review on #4997 (MUL-4134). channel_* has no FK/cascade (MUL-3515 §4), so hard-deleting a different-agent revoked installation left application-owned rows dangling at a removed installation_id: - channel_chat_session_binding: the outbound patcher would resolve a binding, then fail loading the deleted installation — turning a clean no-op into error logs. - channel_binding_token: a still-unexpired bind link (15 min TTL) could be redeemed into the deleted installation, reporting "bound" against a bot that no longer reaches the user. - channel_inbound_audit: dangling installation_id, where migration 124 models the old ON DELETE SET NULL as an app-layer NULL. - channel_user_binding: dead member links (a different agent is a distinct connection; links do not follow and can never be reused). Rework RemoveRevokedInstallationByAppID to resolve the single row holding the app_id and act only when it is revoked, in this workspace, and owned by another agent; then, on the caller's transaction, clear chat-session bindings, pending binding tokens and member links, NULL the audit references, and finally delete the row via the fenced query (defense in depth). Same-agent reconnect and active/other-workspace rows are no-ops. Adds DeleteChannelUserBindingsByInstallation, DeleteChannelBindingTokensByInstallation, and NullChannelInboundAuditInstallationID queries, plus a DB-backed test (TestChannelStore_RebindCleansDependentRows) asserting every dependent is cleaned and the audit row survives detached. Verified the test fails when the cleanup is skipped. Co-authored-by: multica-agent <github@multica.ai> * fix(lark): make the rebind cleanup race-safe with a guarded delete gate Addresses the concurrency must-fix on #4997 (MUL-4134). The prior shape read the candidate installation, checked revoked/workspace/agent in Go, cleaned the dependent rows, then ran the fenced delete. That read-then- clean-then-delete order has a TOCTOU: while B is rebinding the bot to a different agent, A can reconnect to the SAME agent and reactivate the row to 'active' in between. B still wipes A's user/chat/token bindings and NULLs its audit based on the stale "it was revoked" read, then the fenced delete no-ops (status is no longer revoked) — so A's installation survives active but its bindings are gone. Concurrent same-agent data loss, reintroduced. Make the guarded DELETE the atomic gate. DeleteChannelInstallationByAppID becomes DeleteRevokedChannelInstallationByAppID `:one ... RETURNING id`, and RemoveRevokedInstallationByAppID keys all dependent cleanup off the id the delete actually claimed. No separate read. Under READ COMMITTED a concurrent reactivation makes the DELETE re-check status='revoked' against the live row (EvalPlanQual): it claims nothing, returns pgx.ErrNoRows, and no dependents are touched. With no FK the cleanup can follow the claiming delete in the same transaction; any failure rolls the whole thing back. Adds TestChannelStore_RebindGuardedDeleteRaceWithReactivation: two real transactions race on one revoked installation — one reactivates and holds the row lock, the other runs the rebind cleanup and blocks on the guarded delete — asserting the installation and every binding stay intact. Verified this test fails on the old read-then-clean-then-delete shape and passes (also under -race) on the gated version. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: jiangliangyou <jiangliangyou@xiaomi.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: J <j@multica.ai> |
||
|
|
566d51f1c0 |
perf(chat): fix pending-task index mismatch + cut aggregate request storm (MUL-4159) (#5018)
* perf(chat): fix pending-task index mismatch + cut aggregate request storm (MUL-4159) ListPendingChatTasksByCreator was a top DB hotspot. Root cause: the partial index idx_agent_task_queue_chat_pending (migration 040) only covers status IN (queued, dispatched, running), but migration 109 added a fourth in-flight status (waiting_local_directory) that both pending chat queries now filter on. Postgres can only use a partial index when the query predicate is a subset of the index predicate, so the 4-status query stopped using it and degraded to a Seq Scan over the whole agent_task_queue. Implements the reviewed P0-P3 plan in one PR: P0 index fix (split single-statement CONCURRENTLY migrations per repo convention) - 143: CREATE INDEX CONCURRENTLY idx_agent_task_queue_chat_pending_v2 covering all four in-flight statuses (same column list, so GetPendingChatTask still benefits). - 144: DROP the superseded 3-status index, in its own migration. P1 SQL + handler hot path - ListPendingChatTasksByCreator now returns cs.agent_id and states chat_session_id IS NOT NULL so the planner can prove the partial-index subset. - ListPendingChatTasks filters private-agent access against the already-loaded accessible-agent set using the returned agent_id, dropping the extra ListAllChatSessionsByCreator scan on the hot path. - Regenerated sqlc. P2 frontend request amplification - FAB uses the new boolean has-any query gated on enabled:!isOpen, so the minimised button never holds the full aggregate. - use-realtime-sync maintains the pending aggregate (list + has-any) in place from task lifecycle events (queued/dispatch/running/waiting_local_directory -> upsert; completed/failed/cancelled -> remove) instead of invalidating on every chat:message/chat:done, with a debounced fallback invalidate for reconnect / unknown payloads. P3 boolean endpoint - GET /api/chat/pending-tasks/has-any backed by HasPendingChatTasksByCreator (EXISTS). Permission filtering is baked in via agent_id = ANY($3); an empty accessible-agent set short-circuits to false. The detailed list stays for the ChatWindow history / stop-task flows. Tests: new handler tests cover the private-agent gate on both endpoints (hidden from a creator who lost access, visible to the agent owner) plus the boolean status/terminal semantics. EXPLAIN (ANALYZE, BUFFERS) on a 300k-row reproduction: - before (3-status index): Parallel Seq Scan, ~300k rows filtered, shared hit=3012, 12.1 ms. - after (v2 index): Index Scan on idx_agent_task_queue_chat_pending_v2, shared hit=131, 0.07 ms. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): stop optimistic cross-session pending aggregate writes from workspace-fanout task events (MUL-4159) Review on PR #5018 flagged a real privilege-escalation bug in the P2 change: use-realtime-sync optimistically upserted the cross-session pending aggregate (pendingTasks / pendingTasksHasAny) from chat task:* events. Those events are a workspace fanout delivered to every member (server still BroadcastToWorkspace, see cmd/server/listeners.go), and the payload carries no creator / agent visibility. So member B starting a chat task could flip member A's FAB to has_pending=true, bypassing the server-side permission filter on /api/chat/pending-tasks[/has-any]. Fix (option 1 from the review — the self-contained one): never optimistically write the aggregate from task:* events. On every task lifecycle transition, debounced-invalidate the aggregate so it is refetched through the permission-filtering endpoint, which only returns the caller's own creator-owned, accessible-agent tasks. The per-session pendingTask cache is still written directly — it is keyed by chat_session_id and only rendered for sessions the user can open (server-gated), so it is not a cross-user leak. chat:message is still excluded from aggregate refresh, so the MUL-4159 request storm stays fixed; task transitions are per-task and coalesced by the debounce. - Removed upsertPendingAggregate / removePendingAggregate. - Added exported refetchPendingChatAggregate(qc, wsId) — an invalidate, never a setQueryData — used by the debounced handler. - Regression tests: refetchPendingChatAggregate leaves the cached has_pending/list untouched (no optimistic write) and only invalidates for an authoritative server-filtered refetch; no-ops without a workspace id. Verified: @multica/core + @multica/views typecheck; full core vitest suite (752 tests) green including the 2 new guard tests. Co-authored-by: multica-agent <github@multica.ai> * chore(chat): address review nits on pending-tasks endpoints (MUL-4159) - Restore the GetPendingChatTask godoc first line that was clipped when the has-any handler was inserted (nit#1). - ListPendingChatTasks short-circuits to an empty list when the caller has no accessible agents, mirroring HasPendingChatTasks — skips the DB round-trip (nit#2). - Add a cross-creator negative test: user A's in-flight task on a workspace-visible agent returns has_pending=false / empty list for user B, locking the cs.creator_id tenant gate that the agent-visibility filter does not cover (nit#3). Verified: go build ./... and go test ./internal/handler -run PendingChatTasks (7 tests) green against live Postgres. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e36c0cd404 |
fix: preflight Claude root/sudo launches with an actionable error (#4944)
Detect the root/sudo + bypassPermissions launch condition before starting Claude Code and fail fast with an actionable error (run as non-root, or set IS_SANDBOX=1 in a genuine container/sandbox). Closes #3278 MUL-4095 |
||
|
|
1de0c7d14c |
MUL-4158: allow deleting orphaned profile runtimes
Fixes MUL-4158 |
||
|
|
5dfb0bec06 |
Fix Codex MCP allowlist config rendering (#4949)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a69d969fb1 |
fix(sweeper): gate running-task wall clock on runtime liveness (MUL-4107) (#4978)
The server-side running-task sweeper failed rows purely on `started_at >
now() - runningTimeoutSeconds` (2h30m). By its own comment the wall clock
is "mainly for runs whose daemon died without reporting" and "only needs
to sit generously above any realistic single run" — but the predicate
does not actually distinguish a healthy long-running task from an
orphaned one. On self-hosted deployments this kills multi-hour research
/ training runs mid-flight even though the daemon is still heartbeating
and the run is actively producing output.
The daemon side is intentionally unbounded (only inactivity watchdogs:
idle 30m, tool 2h); the server backstop was silently the only wall
clock. `FailStaleTasks` is now AND-gated on runtime liveness:
* dispatched — unchanged; already excludes rows with a live
`prepare_lease_expires_at` (renewed every 15s by the daemon between
claim and StartTask).
* running — new: excluded when the task's `agent_runtime` row is
`online` AND `last_seen_at` is within the runtime stale window
(staleThresholdSeconds = 150s, the same signal
sweepStaleRuntimes already uses).
Healthy long-running tasks on live daemons are no longer killed by the
wall clock. The daemon-dead case remains primarily handled by
sweepStaleRuntimes in the same tick (Redis LivenessStore + DB stale +
FailTasksForOfflineRuntimes); the wall-clock branch is now a defensive
backstop for the pathological case where a runtime row lingers online
with a stale DB heartbeat for longer than the wall clock. `runtime_id
IS NULL` is treated as "not proving liveness" so the wall clock still
fires on that (rare / historical) shape.
The 2h30m default is unchanged — this is a gate, not a threshold
change. Tests updated: 4 existing running-task tests now age out the
runtime so they still exercise the wall clock; 2 new tests cover both
new invariants (healthy runtime → skipped; stale runtime → still
killed).
Fixes #4958
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
083e045ec6 |
MUL-4103: harden Windows browser MCP config (#4976)
* fix: harden Windows browser MCP config Co-authored-by: multica-agent <github@multica.ai> * fix: address browser mcp review nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f67f0bc9d8 |
fix: block claude settings flag for antigravity (#4974)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e3de28ecd7 |
fix(agent): pi agent final output excludes intermediate steps (#4894) (MUL-4030)
* fix(agent): pi agent final output excludes intermediate steps
Updated PI agent to only retain the final result in JSON output.
Previously, `text_delta` included both intermediate steps and final
content.
Now, output is reset on each `text_start` to concatenate only the
final text.
* fix(agent) Replace `message_update.text_start` with `turn_start` event. add test
`turn_start` begins a new turn, Reset output on it to exclude
intermediate texts.
|
||
|
|
346f818206 |
fix(runtime): add traecli to custom runtime profile whitelist (#4972)
Trae (traecli) already has a New() backend, launch header (traecli acp serve) and provider branding, but was missing from every protocol_family whitelist, so custom runtime profiles based on Trae were rejected and it never appeared in the family picker. Add traecli to SupportedTypes (Go), RUNTIME_PROFILE_PROTOCOL_FAMILIES (TS), the lockstep test's want map, and a new migration 136 widening the runtime_profile_protocol_family_check constraint. MUL-4094, #4945 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3f17c2717b |
WS-1465 fix autopilot duplicate issue dispatch (#4936)
Co-authored-by: JC的AI分身 <tangyuanjc@JCdeAIfenshendeMac-mini.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7116691c07 |
fix(github): hide reference-only PR links from the issue PR list (#4611)
* fix(github): hide reference-only PR links from the issue PR list A PR that merely mentions an issue key in passing in its description (e.g. "Related to MUL-3739") was auto-linked and shown in that issue's right-side PR list as if it were a working PR for the issue. Add a reference_only flag to issue_pull_request. The webhook keeps linking generously (so close_intent stays trackable across edits) but flags a link as reference_only unless the key is a genuine target: a title prefix, a branch reference, or a body closing keyword (Closes/Fixes/Resolves). ListPullRequestsByIssue filters reference_only rows, so passing body mentions are hidden from the CLI and the UI PR list while real targets remain. reference_only follows the same terminal preserve gate as close_intent; the auto-advance gate is unchanged. Closes MUL-3739 Co-authored-by: multica-agent <github@multica.ai> * fix(github): exclude reference_only links from the close aggregate A reference_only link is hidden from the issue PR list, but GetIssuePullRequestCloseAggregate still counted it toward open_count. An open body-only mention ("Related to MUL-X") could therefore block the issue from auto-advancing to `done` after a real closing PR merged, while being invisible in the right-side PR list. Filter `AND NOT reference_only` in the aggregate too (reference_only rows never carry close_intent, so merged_with_close_intent_count is unchanged). Add TestWebhook_HiddenBodyMentionDoesNotBlockAutoAdvance. Addresses code review on PR #4611. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
129efb7688 |
feat(runtime): allow qoder as a custom runtime profile base (#4883) (#4912)
Qoder CN (`qoderclicn`) users could only reach a working custom runtime by misrouting through the Kiro backend, which launches `<cmd> acp --trust-all-tools`. That is incompatible with Qoder's global `--acp` / `--yolo` argv, so the task failed immediately with `kiro initialize failed` and no run messages. Expose `qoder` in the custom-profile protocol_family whitelist across every lockstep layer: - server/pkg/agent SupportedTypes (+ whitelist pin test) - migration 134 runtime_profile protocol_family CHECK (NOT VALID, mirroring 126) - packages/core RUNTIME_PROFILE_PROTOCOL_FAMILIES The existing qoderBackend already honors an ExecutablePath override and launches `<cmd> --yolo --acp`, so a profile with protocol_family=qoder and command_name=qoderclicn now launches with the correct argv instead of the Kiro shape. Provider branding/logo for qoder already exists. MUL-4018 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1ff99e5afc |
fix: honor completed codex turns during process eof races (#4899)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
910bbe9309 |
MUL-4024 tighten squad leader self-trigger guard (#4896)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cb68669c73 |
feat(composio): gate MCP apps behind feature flag (#4876)
* feat(composio): server-side connect flow + connections REST (Notion MVP) (MUL-3720) (#4608)
* feat(composio): server-side connect flow + connections REST (Notion MVP) (MUL-3720)
Compose the merged server/pkg/composio SDK into a user-facing connection
manager: signed-state connect handshake, local user_composio_connection
mirror, idempotent disconnect, and a per-user MCP session helper (not yet
wired into task dispatch).
- migration 127_user_composio_connection (no FK/cascade, per DB rules)
- sqlc queries: upsert (idempotent on user_id+connected_account_id), list
active, owner-scoped get, mark revoked
- internal/integrations/composio: signed HMAC-SHA256 state, BeginConnect,
CompleteCallback (idempotent upsert), ListConnections, Disconnect
(upstream 404 = idempotent success), CreateMCPSession (no-op when empty,
pins connected_accounts per toolkit), CallbackRedirect
- REST handlers under /api/integrations/composio (user-scoped, 503 when
COMPOSIO_API_KEY unset): connect/init, callback (302), connections list,
delete
- router wiring gated by COMPOSIO_API_KEY; COMPOSIO_AUTH_CONFIGS_JSON maps
toolkit->auth_config (MVP: notion); state secret from COMPOSIO_STATE_SECRET
or derived from JWT_SECRET; callback base from COMPOSIO_CALLBACK_BASE_URL
or MULTICA_PUBLIC_URL
- tests: state (expire/tamper/wrong-secret), service (mapping, callback
idempotency, non-success, disconnect owner/404 idempotency, MCP pin),
handlers (httptest), redact regression for Bearer mcp_ tokens
MVP scope: Notion only; no task-dispatch overlay, sharing, or webhook
event handling (later stages).
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): bind callback account to user + idempotent revoked disconnect (MUL-3720)
Address PR 4608 review (CHANGES_REQUESTED):
- callback: verify connected_account_id with Composio before mirroring it.
The signed state only proved user/toolkit/exp, so a valid state paired with
a tampered connected_account_id would be written verbatim. CompleteCallback
now calls ListConnectedAccounts and fails closed (ErrAccountVerification)
unless the account belongs to the state's user (composio_user_id == multica
user id) and was created under the toolkit's auth config. No row is written
on mismatch / unknown account / upstream error.
- disconnect: short-circuit to a no-op when the local row is already revoked,
before touching upstream. Previously a second DELETE re-hit Composio and a
non-404 upstream error surfaced as a 502, breaking the 204-idempotent
contract.
- CreateMCPSession: document the v1 single-active-connection-per-(user,toolkit)
constraint and make duplicate selection deterministic (newest-wins, rows are
connected_at DESC) instead of order-dependent map overwrite. Stage 3 owns the
real single-account-enforcement vs multi-account-shape decision.
Tests: tampered/wrong-auth-config/unknown-account callback rejection, revoked-row
disconnect no-op (asserts upstream not re-hit). composio pkg 85% coverage; all
green.
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): list all toolkits + dynamic auth-config resolution (MUL-3720)
Yushen's follow-up to the Notion MVP: surface the full Composio toolkit
catalog, render it in Settings, and drop the static env mapping in favor of
dynamic auth-config discovery.
Config correctness (per Composio docs):
- Remove COMPOSIO_AUTH_CONFIGS_JSON entirely. The toolkit→auth_config mapping
is now resolved at request time from the project's /auth_configs (cached,
5-min TTL), so enabling a toolkit is a dashboard action, not a redeploy.
- Do NOT add COMPOSIO_PROJECT_ID. The project API key (x-api-key) authenticates
to exactly one project; the project is resolved from the key. Only org-level
endpoints use x-org-api-key, which this integration never calls.
Backend:
- SDK: server/pkg/composio/auth_configs.go — ListAuthConfigs (toolkit_slug,
is_composio_managed, show_disabled, limit, cursor).
- service: dynamic resolver (authConfigMap cache; betterAuthConfig prefers a
custom/white-label config over Composio-managed, newest wins); BeginConnect
and CompleteCallback resolve via it; ListToolkits fetches the full catalog
(paginated, capped) annotated with connectable = has an enabled auth config,
connectable-first ordering.
- handler + route: GET /api/integrations/composio/toolkits (user-scoped, 503
when COMPOSIO_API_KEY unset) returning slug/name/logo/category/connectable.
Frontend:
- core: ComposioToolkit/ComposioConnection types, api client methods, and
composio query options (@multica/core/composio).
- views: Settings → Integrations now has a Composio section rendering every
toolkit as a card with search. Connect is gated on `connectable`;
non-connectable toolkits show a muted "not configured" hint instead of a
dead button. Connected toolkits show a badge + Disconnect (with confirm).
- i18n: composio block added to en/zh-Hans/ja/ko settings.
Tests: SDK + service (dynamic resolution, custom-over-managed preference,
connectable flag, resolver-error soft-degrade) and handler toolkits endpoint;
composio pkg 85.7% coverage. go build/vet/gofmt clean; core+views typecheck,
core+views lint, and core tests (691) all green.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): close cross-toolkit callback fail-open by signing auth_config_id into state (MUL-3720)
Re-review blocker: CompleteCallback resolved the toolkit's auth config at
callback time and ignored a resolve error/empty result, while
verifyAccountOwnership skipped the auth-config comparison when the expected
value was empty. A user could then pass another toolkit's connected_account_id
into this toolkit's callback — the owner check passed and it was written under
the wrong toolkit_slug/account binding.
Fix: the auth_config_id is already resolved in BeginConnect (before the state
is signed), so sign it into the state and compare it exactly at callback. No
re-resolve, no fail-open. verifyAccountOwnership now fails closed when the
expected auth config is empty (rejects instead of skipping) and requires an
exact match — closing the cross-toolkit binding gap.
Tests: state round-trips auth_config_id; BeginConnect signs it; callback
rejects wrong/cross-toolkit auth config and an empty (no-mapping) auth config
fails closed. composio pkg 85.2% coverage, all green.
Frontend (non-blocking): the Composio settings tab now surfaces an error when
the connections query fails instead of silently rendering everything as
unconnected.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): hide Settings section entirely when integration unconfigured (MUL-3720)
Decision (option 2, hide-then-merge): don't show a card that leaks the internal
COMPOSIO_API_KEY env-var name to every end user. IntegrationsTab now gates the
whole Composio section (heading + body) on the toolkits query — a 503 means the
key is unset, so the section is withheld instead of rendering the not-configured
card. Admin-only setup guidance is a later, role-gated affordance.
Removed the notConfigured card (and now-unused ApiError import) from
ComposioTab; it only mounts when configured. views typecheck + lint clean.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): Stage 2 frontend polish — callback toast, last_used & expired UI, e2e (MUL-3718) (#4688)
* feat(composio): callback toast + refresh, last_used & expired UI, e2e (MUL-3718)
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): real callback redirect route + StrictMode-safe toast dedup (MUL-3718 review)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): callback endpoint should not require Multica auth (MUL-3843) (#4709)
* fix(composio): move OAuth callback out of the Auth group (MUL-3843)
Composio 302-redirects the browser to /api/integrations/composio/callback
at the end of the OAuth flow, but PR #4608 mounted it inside the cookie-auth
middleware group. When the session cookie is absent (expired session,
SameSite=Strict / Safari ITP, private window, self-hosted callback subdomain)
the Auth middleware returned a hard 401 and a JSON blob instead of the
settings redirect, breaking the flow.
Identity never came from the cookie anyway: it is carried by the HMAC-signed
state param that CompleteCallback verifies (signature, expiry, replay) and
cross-checked by verifyAccountOwnership; h.Composio == nil still 503s. So the
callback is registered alongside the other public OAuth/webhook routes; the
other four composio endpoints stay session-gated.
Refs MUL-3843, MUL-3715.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): correct stale callback routing comments (MUL-3843)
The package header and ComposioCallback doc comments still described the
callback as sitting under the Auth middleware group. After the route was
moved out (this PR), update both to state it is a public route whose identity
comes from the signed state — addressing review nit from 张大彪.
Refs MUL-3843.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): inject MCP overlay into agent runtime at task dispatch (MUL-3721) (#4704)
Stage 3 of the Composio epic. Wires the per-user Composio MCP session into
every agent task so the agent process sees the initiator's connected tools
without any prompt-time plumbing.
Server side
- Migration 128 adds agent_task_queue.runtime_mcp_overlay JSONB plus a
BEFORE-UPDATE trigger that wipes the column on any transition into a
terminal status (completed / failed / cancelled). A trigger is the single
source of truth — future queries that flip status cannot bypass it.
- composio.Service.BuildTaskOverlay(userID) reuses CreateMCPSession and
emits the Claude-style { mcpServers: { composio: { type: http, url,
headers } } } shape the daemon's existing sidecar generators consume.
Returns (nil, nil) on zero active connections so we never burn a
Composio session for a user with nothing to call.
- TaskService grows a Composio ComposioOverlayBuilder seam, wired in
router.go after composiointeg.NewService succeeds. Five enqueue paths
(issue / mention / quick-create / chat / auto-retry) attach the overlay
after CreateAgentTask returns and before the daemon is notified — so
every claim reads a settled row, with no second daemon hop. Best-effort:
a builder failure logs and proceeds with no overlay.
- resolveInitiatorFromTriggerComment derives the initiator user from the
trigger comment when it was authored by a member. Agent-authored
triggers are not treated as initiators (their connected-apps view is
empty by construction).
Daemon side
- handler/daemon.go claim path merges task.runtime_mcp_overlay onto
agent.mcp_config via mergeMCPOverlay before populating
TaskAgentData.McpConfig. Overlay wins on server-name collisions
because it carries the live user-scoped session URL. Errors fall back
to the agent config unchanged — a bad overlay must not surprise-disable
saved MCP tools. The existing execenv sidecar generators (cursor /
codex / openclaw / opencode / hermes-kiro) need no changes: they keep
consuming the merged result through TaskAgentData.McpConfig.
Tests
- 9 merge cases (mcp_overlay_test): both-nil short-circuit, agent-only
pass-through, overlay-only canonicalization, two-side merge, name
collision (overlay wins), top-level key preservation, malformed agent
fallback, malformed overlay fallback, non-object server rejection.
- 4 dispatch cases (composio): zero-connections returns nil without
CreateSession, happy-path emits the right shape with the right user
id, empty-URL defensive branch, SDK error surfacing.
- 4 TaskService helper cases: nil Composio is a no-op (Queries-safe),
invalid initiator does not call the builder, nil overlay skips the
UPDATE, builder error swallowed without panic.
- Migration 128 verified to roll up + down + up cleanly against the test
database.
Out of scope (deferred): assignment-triggered enqueue paths with no
trigger comment get no overlay attached today (no initiator UUID flows
through enqueueIssueTask in that case). Retry paths recompute the overlay
fresh from the parent's initiator_user_id instead of inheriting the bearer
from the parent row, so a stale token can never resurface on a retry.
Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): per-agent allowlist + originator-scoped MCP overlay (MUL-3869) (#4736)
* feat(composio): per-agent allowlist + originator-scoped MCP overlay (MUL-3869)
Stage 3.1 of the Composio epic (MUL-3721 parent). PR #4704 wired in the
runtime_mcp_overlay column and a per-task dispatch hook; this change
inverts the default from "all-on" to opt-in and locks the overlay to the
agent owner's own connected apps:
- Agents carry composio_toolkit_allowlist TEXT[]. NULL or [] => no MCP.
Owner-only read/write; non-owner GET/PUT silently redacts/drops the
field (same shape as mcp_config).
- agent_task_queue carries originator_user_id UUID. Set from the
top-of-chain HUMAN at every enqueue path:
* issue/mention comment by member -> author_id
* issue/mention comment by agent -> inherit via comment.source_task_id
-> parent task originator_user_id
* quick-create -> requester_id
* chat -> initiator_user_id
* retry -> SQL-inherited from parent row
* autopilot -> NULL (system-driven)
- BuildTaskOverlay (composio dispatch) now takes (ctx, originatorUserID,
agent) and short-circuits on five gates: invalid originator,
originator != agent.owner_id, empty allowlist, empty intersection of
allowlist ∩ active connections, defensive empty session URL. Composio
CreateSession is called with BOTH `toolkits.slugs` (the intersection)
AND `connected_accounts` (the pinned account ids), narrowing the
tool-router twice.
- The originator-vs-owner gate closes the agent-fanout privacy hole: any
workspace member who can @-mention a public agent used to project the
owner's connected apps into their run. Now the overlay only mounts
when the human at the top of the chain IS the agent owner.
Tests:
- dispatch_test.go covers all 5 gates plus uppercase/whitespace slug
normalisation.
- task_runtime_mcp_overlay_test.go covers the no-op gates of the new
applyRuntimeMCPOverlay signature.
- agent_composio_allowlist_test.go (handler): owner roundtrip
(list/empty/null), workspace-admin silent-drop, owner-only GET
visibility, pure normaliseComposioToolkitAllowlist.
- resolve_originator_test.go (service, DB-backed): member-authored,
agent-authored inherits via comment.source_task_id, invalid id.
Migration 129 up/down/up verified against docker postgres.
Co-authored-by: multica-agent <github@multica.ai>
* chore(composio): gofmt + regenerate sqlc with v1.31.1 (MUL-3869 review nits)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): accept nested connected account auth config
* feat(views): creator-only MCP tab for per-agent Composio allowlist (MUL-3870) (#4743)
Stage 3.2 frontend on top of the Stage 3.1 backend (MUL-3869,
|