mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 22:59:04 +02:00
v0.4.0
669 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> |
||
|
|
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> |
||
|
|
eacc842280 | fix(agents): constrain builder models to runtime (#5323) | ||
|
|
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> |
||
|
|
4ee34b9fcb |
fix: return stage from issue list APIs
Closes MUL-4393 |
||
|
|
0b054b5c8b |
fix(chat): clear external channel binding on archive, drop archived from FAB unread (#5214)
Archiving a channel-bound chat session now severs its channel_chat_session_binding in the same tx as the status flip. The web send path already treats status='archived' as read-only, but the channel engine (Feishu/Slack) resolves inbound traffic through the binding without checking session status, so an archived-but-still-bound session kept receiving agent replies and a stuck, uncleared unread badge. Dropping the binding makes the next inbound message fork a fresh session; unarchive does NOT recreate it (a later session may already own the channel). The FAB unread badge counted status=all sessions without excluding archived, so residual unread on an archived session (archive does not mark-read) held the badge even though the session is hidden and read-only. Extract countUnreadChatSessions() and exclude archived. This matches what mobile already computes (active-only list), restoring count parity. Tests: backend archive-clears-binding + unarchive-does-not-recreate; frontend countUnreadChatSessions archived-exclusion cases. MUL-4372 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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) | ||
|
|
9d453fac1e |
fix(comments): clarify 409 for top-level comment from a comment-triggered task (MUL-4417) (#5292)
* fix(comments): clarify 409 when a comment-triggered task posts a top-level comment (MUL-4417) A comment-triggered task that posted a parentless top-level comment on its own issue got a 409 whose message named the required parent id but never said top-level comments are disallowed. Agents misread it as the issue being locked and deleted good replies trying to reset. Keep the guard (agents must reply under their trigger comment), but make the error self-explanatory and document the constraint in the CLI --parent help. Add handler-level tests pinning the rejected top-level case and the allowed reply-under-trigger case. Refs GH #5266. Co-authored-by: multica-agent <github@multica.ai> * fix(comments): tighten 409 wording and assert the fix hint (MUL-4417) Review nits on #5292: drop the inaccurate "while it is active" phrasing and the redundancy from the 409 message so it matches the actual allow-set (trigger or coalesced comment); collapse the incident narration to one line; and assert the actionable parent_id (--parent) hint in the regression test so the guidance can't be dropped silently. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J (Multica agent) <agent-j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
05d9298582 |
fix(agents): let workspace members view runtime capabilities (MUL-4427) (#5281)
* fix(agents): let workspace members view runtime capabilities (MUL-4427) The Agent capabilities redesign (#5277) reused the runtime local-skills discovery endpoint on Agent detail surfaces, but the endpoint kept the owner-only gate from the original import flow. Viewing an agent bound to someone else's runtime returned 403, which the Skills / MCP tabs rendered as 'try again when the runtime is online' even though the runtime was online. - Discovery (list + poll) now requires workspace membership only; the payload is the deliberately redacted inventory built for this display. - Import (init + poll) stays owner-only: it copies skill file contents off the owner's machine. - The failed notice no longer blames runtime connectivity, and a 403 (new client against an older backend) gets an honest permission message. * test(settings): stub Intl.supportedValuesOf in timezone picker tests The preferences-tab timezone tests drove a ~600-option Base UI Select through userEvent in jsdom; on slow CI runners the clear-preference case exceeded even its extended 20s per-test timeout (PR #5281 frontend job). Stub the IANA enumeration down to the curated COMMON_TIMEZONES fallback — everything the tests pick lives there too — and drop the now-unneeded 20s overrides. File test time drops from ~35s to under 1s. --------- Co-authored-by: Lambda <lambda@multica.ai> |
||
|
|
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> |
||
|
|
3c417ea631 |
fix(MUL-4348): authorize + chronologically order per-thread coalesced replies (#5211)
Testing surfaced two problems with the per-thread fan-out:
1. Authorization (blocker): CreateComment rejected any agent comment on the
task's issue whose parent_id != task.TriggerCommentID, so replies to the
OTHER coalesced threads were denied ('parent_id must equal this task's
trigger comment id') and those threads never got a reply. Allow the trigger
comment OR any comment the task coalesced (taskCoversReplyParent: trigger ∪
coalesced_comment_ids); every other parent on the issue is still rejected,
so this stays scoped to the set the run was actually given to answer.
2. Ordering: the agent answered the newest (triggering) comment first. The
fan-out instruction now numbers the targets and explicitly requires posting
OLDEST thread first, the newest/triggering thread last, so replies land in
chronological order. commentReplyThreads already lists oldest-first.
Tests: TestTaskCoversReplyParent (allow-list) and chronological-order
assertions in the cross-thread prompt test.
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
6c3b79db19 |
feat(daemon): bound daemon.log size with rotation (MUL-4330) (#5170)
* feat(daemon): bound daemon.log size with rotation (MUL-4330) The background daemon redirected its stdout/stderr into daemon.log opened O_APPEND and never rotated it, so the file grew without limit until it was too large to open. Every structured log line already flows through slog (including agent subprocess stderr, forwarded via newLogWriter), so the daemon's logger is effectively the sole author of the file's volume. Route the foreground daemon's slog output — both the injected component logger and the package-global slog default — through a size-based rotating writer (lumberjack) that keeps the active daemon.log small (20MB default, 5 gzip-compressed backups, 30d), all env-overridable. Raw crash output (Go runtime panics, pre-logger errors) now goes to a separate daemon.err.log so the child's inherited fds never hold daemon.log open, which would block rotation's rename on Windows. The Desktop app spawns the daemon via this same launcher and its log tail already handles size-shrink, so both CLI and Desktop are covered. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): address log-rotation review — foreground output, Windows handles, bounded err log (MUL-4330) Resolves the blocking review items on the daemon.log rotation change: 1. Windows first-upgrade rotation: a foreground managed daemon now re-points its own stdout/stderr to daemon.err.log at startup (SetStdHandle) before building the rotator, releasing any daemon.log handle an older self-update launcher inherited (Go opens files without FILE_SHARE_DELETE, which would otherwise block rename-on-rotate). No-op on Unix, where an open fd never blocks rename. 2. `daemon logs -f` vs rotation: Unix uses `tail -F` (reopen by name); Windows opens the reader with FILE_SHARE_DELETE so it can't block the rotator's rename, and reopens the file on size-shrink to follow across rotation. 3. Self-update handoff no longer briefly runs two rotators on one file: the old process closes its rotator and moves remaining handoff logs (incl. the slog default) to the crash sink before the successor starts. 4. daemon.err.log is now bounded: it rolls to a single ".1" backup once past 5MB at open time, so a crash loop can't move the growth problem to it. It is also surfaced in the troubleshooting docs. 5. Explicit `--foreground` in a terminal keeps live stdout/stderr logging (a documented debugging path); only detached/background children rotate into daemon.log. Decided by whether stderr is a terminal. Also: rotation env knobs now reject 0/negative (0 means 100MB / keep-all in lumberjack), preventing an accidental unbounded config. Adds unit tests for the err-log rolling and positive-int parsing; Windows/Linux(arm64) cross-builds and `GOOS=windows go vet` pass. Co-authored-by: multica-agent <github@multica.ai> * docs: sync zh/ja/ko troubleshooting with daemon.log rotation + daemon.err.log (MUL-4330) Co-authored-by: multica-agent <github@multica.ai> * test(handler): bump the agent's own runtime version in quick-create parent test (MUL-4330) TestQuickCreateIssueParentTrustBoundary bumped an arbitrary `LIMIT 1` agent_runtime, but the handler version-checks agent.RuntimeID — the runtime bound to the request's agent. In the shared handler test workspace, other tests register additional runtimes, so the two diverge and the agent's real runtime keeps the seed's empty cli_version, tripping the daemon-version gate (422 daemon_version_unsupported) before the parent_issue_id assertions run. Bump the runtime tied to the agent instead, making the setup deterministic. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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> |
||
|
|
cc3daaf3b4 |
fix: scope claim-time comment fetch to workspace + guard --attachment paths (MUL-4252) (#5190)
* fix(daemon): scope claim-time comment fetches to the task's workspace (MUL-4252) The daemon claim path embeds the triggering comment and every coalesced comment's full text into the agent prompt, but fetched them with an unscoped `GetComment(id)` — a task row carrying a foreign comment UUID would pull another workspace's comment text into the prompt. On a shared SaaS backend (tens of thousands of workspaces in one DB) that is a tenant boundary hole, latent today only because task rows are server-written. Switch all three claim/reconcile GetComment calls to GetCommentInWorkspace, scoped by the runtime's workspace (claim path) or the issue's workspace (completion reconcile). The task's issue workspace is already asserted equal to the runtime workspace, so same-workspace delivery is unchanged; a foreign UUID now resolves to "missing" and is skipped — matching buildCoalescedCommentData's documented behavior. Adds DB-backed claim tests: same-workspace trigger comment is still delivered; a foreign-workspace comment's content never surfaces. Co-authored-by: multica-agent <github@multica.ai> * fix(cli): extend the workdir guardrail to --attachment paths (MUL-4252) #5167 fenced --description-file/--content-file to the working directory but left --attachment uncovered — the same /tmp stale-file leak in image form: an agent that writes chart.png to a machine-shared path and attaches it could upload another run's (possibly another workspace's) stale file. Apply ensureAttachmentWithinWorkdir to each local --attachment path in `issue create` and `comment add` (URL values are still skipped upstream), reusing #5167's symlink-resolving fileWithinWorkingDir and the existing --allow-external-file escape hatch. Rejection happens before the issue is created, so a bad path never yields a half-created issue. Co-authored-by: multica-agent <github@multica.ai> * fix(service): scope trigger-summary + originator resolution to the task's workspace (MUL-4252) PR review P1: the claim-time full-comment fetch was already scoped, but the trigger_summary snapshot (first ~200 chars) still leaked. On the real enqueue/merge paths a foreign comment UUID flowed through buildCommentTriggerSummary / resolveOriginatorFromTriggerComment, which used an unscoped GetComment; the truncated text was stored on the task row and later returned in the claim / task-history response (handler/agent.go trigger_summary). Thread the issue's workspace through both helpers (and their exported merge-path wrappers) and switch to GetCommentInWorkspace, so a cross-workspace comment resolves to "missing": trigger_summary stays NULL and no foreign originator is inherited. Every caller already has the issue's WorkspaceID in scope (enqueue, mention/leader, deferred fallback, merge, completion reconcile). Rework the claim test to drive the REAL TaskService.EnqueueTaskForIssue path (which snapshots the summary) and assert the stored row's trigger_summary + originator_user_id stay NULL and the claim response carries neither the foreign body nor the foreign summary. Verified the test fails when the summary fetch is left unscoped. Co-authored-by: multica-agent <github@multica.ai> * fix(cli): validate all --attachment paths before uploading any in comment add (MUL-4252) PR review P2: `issue comment add` checked-read-uploaded each attachment in one loop, so a valid workdir attachment followed by an invalid (external / symlink-escaping) one uploaded the first file — orphaning it as an issue-level attachment — then aborted before posting the comment, and a retry duplicated it. Extract the URL-filter + workdir-guard + read step `issue create` already used into a shared collectLocalAttachments helper and have comment add use it: every attachment is validated and read up front, and nothing is uploaded unless all pass. Adds a command-level test asserting a valid-then-external attachment pair aborts with ZERO upload requests and no comment (fails against the old interleaved loop). Co-authored-by: multica-agent <github@multica.ai> --------- 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> |
||
|
|
302662aee3 |
fix(issues): batch status applies directly; coalesce staged parent notifications (MUL-4155) (#5151)
Batch sub-issue status changes triggered two wrong behaviours from one user action: - Frontend popped the pre-trigger "现在开始处理?" confirm modal for every non-backlog target, but done/cancelled can never start a run, so it degenerated into a misleading "won't start → OK" step. handleBatchStatus now applies directly (product decision: batch status, including backlog → active promotion, applies like a single-issue/CLI change). Assign agent/squad and delete still confirm. The now-unreachable status mode is removed from RunConfirmModal and its locale keys. - Backend evaluated the stage barrier per-child inside the batch loop, using a mid-batch sibling snapshot. A batch closing several stages at once emitted one comment per intermediate stage, pinned the parent assignee's wake to a stale "advance Stage N+1" instruction (the accurate wake was swallowed by the pending-task dedup), and the outcome depended on issue_ids order. BatchUpdateIssues now collects terminal transitions and evaluates each parent once against the batch's final state (notifyParentsOfBatchChildDone): at most one accurate comment + one wake per parent, order-independent. Single-issue UpdateIssue is unchanged; WillEnqueueRun is untouched. Tests: cross-stage batch done/cancelled (forward + reverse) and lower-stage-only on the backend; status-direct / assign-confirm / delete-confirm routing on the frontend. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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> |
||
|
|
86c3f30524 |
fix(server): keep originator on agent-created issues so A2A mentions stay authorized (MUL-4305) (#5149)
* fix(server): attribute agent-created issues so downstream A2A mentions keep the originator (MUL-4305) An agent creating an issue via the ordinary `issue create` path left the new issue with no origin link, so resolveOriginatorForIssueTask could not recover the top-of-chain human. Any assignment / squad-leader run derived from that issue lost the originator, and A2A @-mentions those runs emitted failed the canInvokeAgent gate against private agents (after MUL-3963). Fix (mirrors the comment.source_task_id stamp from MUL-4015): - CreateIssue stamps origin_type='agent_create' + origin_id=<acting task>, resolved from the SERVER-trusted X-Task-ID (never a client-reported field). - resolveOriginatorForIssueTask inherits the origin task's originator for agent_create just like quick_create. - Align the squad-leader gate originator with the enqueue path via the new exported OriginatorForIssueTask, so the gate and the persisted task row agree instead of drifting to an empty originator for agent-triggered assigns. - Migration 149 adds 'agent_create' to issue_origin_type_check. - Classify agent_create in analytics to avoid the unknown-origin warning. Tests: agent_create attribution + gate/enqueue consistency (service), and the HTTP boundary stamp + security regression (member / forged X-Agent-ID must not smuggle an agent_create origin). Co-authored-by: multica-agent <github@multica.ai> * test(server): add end-to-end regression for agent-created issue originator chain (MUL-4305) Locks the real product path the layered tests could miss, per PR review: 1. CreateAssignSquad_PrivateWorkerTriggered — human H triggers agent A → A creates an issue via the ordinary create path AND assigns it to a squad whose leader is a private agent owned by H → the leader's assignment run @-mentions a second private agent J (owned by H) → asserts the leader task carries H and J ends up with a queued task attributed to H. This is the exact line-failure shape from the issue. 2. UpdateAssignSquad_HandlerGateAdmitsPrivateLeader — agent A creates an unassigned issue then assigns it to a private-leader squad via UpdateIssue, exercising the handler enqueueSquadLeaderTask gate (which the create path's ungated service enqueue does not hit); asserts the leader task is enqueued carrying H. Both wire handler create stamp → origin resolution → squad-leader gate → comment source-task stamp → private-worker invocation gate. Verified they FAIL against a simulated pre-fix resolver (leader originator empty; private worker / leader get 0 tasks) and PASS with the fix. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> 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> |
||
|
|
fd3216fd6b |
feat(lark): let an agent's owner bind/manage its Lark bot (MUL-4213) (#5079)
* feat(lark): let an agent's owner bind/manage its Lark bot (MUL-4213) Scan-to-bind was authorized by workspace role only, so a non-admin member could not bind a Lark bot even to an agent they own. Authorize the device-flow install, status poll, and revoke by the same rule that governs every other agent-management op — canManageAgent: the agent's owner OR a workspace owner/admin. Backend: - router: begin/status/revoke drop to workspace-member level; the per-agent check moves into the handlers (agent_id is a query param / installation id, which the role middleware can't see). - BeginLarkInstall + RevokeLarkInstallation load the target agent and run canManageAgent. - GetLarkInstallStatus scopes the read to the session initiator or a workspace owner/admin; others get 404 (no existence leak). Session state now carries InitiatorID for this. Frontend: - LarkAgentBindButton takes agentOwnerId and lets the agent owner through (mirrors canEditAgent). - Agent Integrations tab gates Lark per-agent (owner or admin) while Slack stays workspace-admin-only, since its routes are unchanged. Tests: begin/status/revoke authorization (owner, agent owner, unrelated member) on the backend; agent-owner bind visibility on the frontend. Co-authored-by: multica-agent <github@multica.ai> * fix(lark): keep orphan installation revoke available to workspace admins (MUL-4213) RevokeLarkInstallation loaded the bound agent and ran canManageAgent unconditionally, so once the agent was hard-deleted the load 404'd and a workspace owner/admin could no longer disconnect the orphan Lark installation — a documented cleanup path (ListByWorkspace lists orphans; the active-connection query filters them; Settings surfaces "Unknown Agent" Disconnect). Fall back to workspace owner/admin-only revoke when GetAgentInWorkspace finds no agent; agents that still exist keep the owner-OR-admin canManageAgent check. A plain member gains no orphan-row cleanup rights. No FK/cascade — resolved in the application layer. Adds a backend regression test: orphan installation is revocable by a workspace owner but not a plain member. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@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>
|
||
|
|
405d88c1dc |
feat(squad): allow members to create and manage their own squads (MUL-4223) (#5071)
Squad create/manage was gated behind workspace owner/admin, inconsistent with agents and projects which any member can create. Move squads to a creator-scoped model: any member can create a squad and becomes its creator, and manages only the squads they created; owner/admin continue to manage every squad. Backend (server/internal/handler/squad.go): - Add canManageSquad (admin/owner OR creator) and gate UpdateSquad, DeleteSquad, AddSquadMember, RemoveSquadMember, UpdateSquadMemberRole on it (member load + squad load + per-squad check, replacing requireWorkspaceRole). - CreateSquad is now member-creatable. - Add memberCanWireAgent: a non-admin may only wire agents they can @-trigger (canInvokeAgent as themselves) as squad leader (create/update) or worker (add member); admins may wire any workspace agent. Prevents a creator from smuggling an agent they cannot invoke into a squad. Frontend: - squad-detail-page: compute per-squad canManage (admin || creator) and render the inspector, members tab, instructions and archive read-only otherwise, mirroring the agent detail canEdit pattern. - squads-page: per-row actions and the actions column now key off per-squad canManage instead of workspace-admin. Squads stay visible workspace-wide (ListSquads unfiltered); creator transfer is out of scope for this iteration. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c56f081660 | fix(skills): preserve runtime import files (#5066) | ||
|
|
c4997af4d1 |
fix: archive autopilots on delete (#5042)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
77a05fb731 |
Revert "feat(daemon): worktree_pool mode for local_directory (MUL-3483) (#4986)" (#5037)
This reverts commit
|
||
|
|
b6adf23f91 |
feat(api): emit Content-Length header on JSON responses (#5021)
The core writeJSON helpers streamed the body via json.NewEncoder(w).Encode after WriteHeader, which forces net/http into chunked transfer encoding and omits Content-Length. Buffer the marshaled body first, set an accurate Content-Length, then write — so API (and health) JSON responses advertise their exact size. writeMeasuredJSON gets the same header. Adds a test asserting the header matches the on-wire body length. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7bb8076ed0 |
feat(daemon): worktree_pool mode for local_directory (MUL-3483) (#4986)
* feat(daemon): add worktree_pool mode for local_directory (MUL-3483) ## What changed Squad workflows bound to the same `local_directory` resource used to serialise on a single path mutex — a documented pain point from GitHub issue #4377. This introduces an opt-in `worktree_pool` mode on the `local_directory` project resource. When enabled, each task gets its own `git worktree add` under a daemon-managed pool root, so sibling tasks on the same base repo now run truly in parallel while `git worktree add/remove/prune` stays serialised behind a per-repo mutex. ## Shape - `local_directory.resource_ref` gains three optional fields: `mode` ("in_place" default / "worktree_pool"), `pool_root` (defaults to `<parent>/.multica-worktrees/<base>`), `max_parallel` (defaults to 4). Legacy rows are byte-identical after round-trip: the server validator strips the pool fields on the default in_place path so older clients keep behaving exactly as before. - New `WorktreePoolManager` (`server/internal/daemon/worktree_pool.go`) owns pool allocation, per-repo git-metadata mutex, and cleanup. - `acquireLocalDirectoryLockIfNeeded` now branches on the ref's mode. in_place stays on `LocalPathLocker` and the shared tree; worktree_pool routes through the pool manager, publishes a lease keyed by task ID, and pins the agent to the freshly allocated worktree in `execenv.PrepareParams.LocalWorkDir`. - Pool saturation is a structured wait_reason (`worktree_pool saturated (N/M) on <path> (holders: ...)`), retrying on the existing cancel-poll interval — same UX as the historical path-mutex wait. ## Safety guardrails (also known footguns from prior art) - Repos with initialised submodules are refused up front. Multi-checkout of a superproject is explicitly unsupported by `git worktree(1)` BUGS and the per-worktree `modules/` directories bloat disk by pool size ×. - Dirty worktrees are NEVER `--force` removed on release. If the agent left uncommitted changes behind we keep the directory (and free the slot) so users can inspect. This is the failure mode claude-code#55724 documented and the pool must not regress into. - The per-repo mutex covers every `git worktree add/remove/prune` and `submodule status` invocation for a given base, matching the in-process-queue fix Anthropic settled on for claude-code#34645 (`.git/config.lock` races on concurrent add). - Task UUID is the source of truth for both branch (`multica/<uuid>`) and worktree path (`<pool_root>/<uuid>`) so a single agent running multiple worker tasks in parallel can never collide. - Non-empty leftover directories at the target path abort the allocation instead of silently starting the agent in an unknown state. ## Explicit MVP non-goals (deferred, tracked as follow-up work) - Windows worktree-remove retry (permission-denied on locked handles). - Detached-HEAD fast path for read-only exploration tasks. - `post-checkout` hook opt-out / serialisation. - Automatic `git lfs install`. - UI surfacing of the pool state / dirty worktree list. ## Tests - `worktree_pool_test.go` (new): full acquire→release lifecycle, parallel allocation, saturation with holder list, slot re-use after release, dirty-worktree preservation, concurrent-acquire serialisation (the config.lock guard), submodule refusal, missing base rejection, pool root auto-mkdir, non-empty leftover refusal, ctx cancel. - Handler validator gains three rejection cases (unknown mode, relative pool_root, negative max_parallel) and a round-trip test that pins the normalised JSON shape for both modes. - Daemon `localDirectoryRef` helpers get a defaults test and the pool root path derivation is pinned. ## Wire-compat and rollout - Default off. Existing rows keep the historical shape (no `mode`, `pool_root`, or `max_parallel` in the JSON) and behave exactly as before. - Opt-in via `--ref '{"local_path":"...","daemon_id":"...","mode":"worktree_pool"}'` today. CLI flag shortcuts (`--mode`, `--pool-root`, `--max-parallel`) can follow in a small tail PR — not blocking. - No DB migration. No UI change required. Co-authored-by: multica-agent <github@multica.ai> * feat(daemon): address worktree_pool review nits (MUL-3483) Follow-up to #4986. Three non-blocking review points from GPT-Boy: 1. **Daemon integration test for lease → runTask plumbing.** `TestAcquireLocalDirectory_WorktreePoolPublishesLease` (and its in_place counterpart) pin the exact contract runTask relies on when it reads `d.localLeases.Load(task.ID)` and feeds `lease.WorkDir` into `execenv.PrepareParams.LocalWorkDir`. A future refactor that drops the Store, mistypes the key, or swaps back to `assignment.AbsPath` on the pool branch will now fail here rather than silently defeat the whole point of worktree_pool mode. 2. **Untracked-only dirty case now classifies as dirty.** `worktreeIsDirty` used `--untracked-files=no`, which meant a worktree with only untracked files was reported "clean" and hit the `git worktree remove` branch — git itself would then refuse the removal because the file exists (so no data was lost), but the log path lied about what happened on disk. Switching to `--untracked-files=normal` routes agents' fresh drafts directly through the "leaving on disk for user inspection" branch, and `TestWorktreePool_UntrackedOnlyIsKept` pins the guarantee so nobody quietly reverts the flag later. 3. **Skill doc note on default `pool_root` location.** `multica-projects-and-resources/SKILL.md` now spells out the three new ref fields (`mode`, `pool_root`, `max_parallel`), the default `<parent>/.multica-worktrees/<repo>` location (next to the repo, not inside it), the write-permission requirement on the parent directory, and the submodule restriction — so agents advising self-host users hit the right doc line rather than reading source. Existing test suite still green: - `go vet ./...` clean - `go test ./internal/daemon/... ./internal/handler/... ./internal/service/...` all pass Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@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> |
||
|
|
30d3aca600 |
feat(attachments): support HTTP Range resume on proxy download (MUL-3962)
Add HTTP Range support to the attachment proxy-download path so an interrupted download can resume from where it left off instead of restarting at byte 0. - Seekable backends (local disk) delegate to http.ServeContent for full Range / If-Range / 206 / Content-Range / 416 handling. - Forward-only backends (S3/MinIO streaming) get a single-range fallback that advertises Accept-Ranges and serves 206 + Content-Range, with a rangeParseOutcome that returns 416 only for genuinely unsatisfiable byte ranges and otherwise ignores unsupported/empty-object ranges (full 200), matching the seekable path. Closes #4831. MUL-3962. |
||
|
|
1de0c7d14c |
MUL-4158: allow deleting orphaned profile runtimes
Fixes MUL-4158 |
||
|
|
39ccb7d342 |
fix(server): do not cancel issue tasks on assignee change (MUL-4113, #4963) (#4975)
* fix(server): don't cancel issue tasks on assignee change (#4963) Changing an issue's assignee previously called CancelTasksForIssue, which cancels every active task on the issue by issue_id alone — regardless of which agent owns the task or how it was triggered. In a multi-agent workspace this silently dropped unrelated in-flight work (a mention-triggered run for another agent, a squad task) with no requeue, and it self-cancelled a run that reassigned the issue from inside its own turn (the daemon then interrupted the live run before its post-handoff cleanup could finish). Reassignment now cancels nothing: ownership handoff no longer implies interruption. The new assignee's run, if any, is still enqueued by WillEnqueueRun and runs alongside whatever was already in flight. Explicit terminal actions — issue -> cancelled and delete issue — still cancel active tasks, unchanged. Applies to both UpdateIssue and BatchUpdateIssues. Adds handler tests that fail against the old behavior (both the previous assignee's own run and an unrelated agent's run got cancelled) and pass now. MUL-4113 Co-authored-by: multica-agent <github@multica.ai> * test(server): cover agent→agent reassign; fix stale WillEnqueueRun comment Addresses review nits on #4975 (MUL-4113, #4963): - Rewrite the outdated WillEnqueueRun doc comment. The assign source no longer cancels existing tasks, so the old "assign cancels existing tasks before enqueuing, pending task moot" premise is wrong. Describe the real invariant instead: the write is guarded by the (issue_id, agent_id) partial unique index, only the status source needs the pending-task dedup, and the assign source safely skips it. - Add a handler test for the core agent→agent handoff path. The existing no-cancel tests only reassigned to a member; this one reassigns from one agent to another and asserts both effects independently: the previous agent's running task survives (no collateral cancel) and the new assignee still gets exactly one run enqueued. MUL-4113 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b2db309618 |
Skip local directory lock for squad leaders (#4951)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
359ef61dc3 |
fix(search): pg_trgm index fallback + statement_timeout guard (MUL-4059) (#4925)
* fix(search): add pg_trgm index fallback + statement_timeout guard (MUL-4059) Root cause of the "search freezes with no response" symptom reported in MUL-4059: the search handler runs LOWER(col) LIKE '%pattern%' queries that expect a pg_bigm GIN index (migrations 032, 033, 036), but every migration wraps the CREATE EXTENSION + CREATE INDEX in a DO/EXCEPTION handler that silently skips when pg_bigm is unavailable. The bundled self-host / dev / CI Postgres image (pgvector/pgvector:pg17) does not ship pg_bigm, so on every self-hosted deployment the migrations no-op and no GIN indexes get built. Every /api/issues/search + /api/projects/search request then falls back to a Seq Scan on `issue` + correlated Seq Scans on `comment` — verified with EXPLAIN on the local dev DB, which has zero title/description/comment search indexes before this change. Two independent guardrails are added, either of which alone would have prevented the reported hang: 1) Migration 134 installs pg_trgm (ships in all standard Postgres + pgvector images) and builds GIN indexes with gin_trgm_ops on `LOWER(title)`, `LOWER(COALESCE(description, ''))`, and `LOWER(content)`. The expression signatures match the search handler's WHERE clauses exactly, so the planner picks the index without further changes. The pg_bigm indexes from 036 are left intact — deployments on AWS RDS with pg_bigm 1.2 keep the CJK-friendly bigram path; deployments without it get the trigram fallback. Verified against a local 25k-row fixture: the description LIKE hits `Bitmap Index Scan on idx_issue_description_trgm` in 0.5 ms. 2) runSearchQuery wraps both search handlers in a short-lived read-only transaction with SET LOCAL statement_timeout = 3 s. In the pathological case where indexes are still missing or the query plan is bad, callers see a fast 503 with a descriptive error instead of a stalled request. Verified against a live Postgres: a deliberate pg_sleep(2) with the test override at 200 ms is cut off in 230 ms with SQLSTATE 57014, as asserted by TestRunSearchQuery_StatementTimeoutFires. Non-goals: this change does not remove the pg_bigm code path, does not change the SQL the handler builds, and does not change the API response shape. It is the minimum diff to unblock production while preserving the CJK-search advantage that pg_bigm provides where it is available. Co-authored-by: multica-agent <github@multica.ai> * fix(search): scope comment subqueries to workspace to unblock prd hang (MUL-4059) Follow-up correction after PRD investigation: pg_bigm IS installed on prd `multica-prod` and all five bigm indexes exist in the correct `LOWER(...) gin_bigm_ops` form. The initial "missing index" hypothesis was wrong; migration 134 (pg_trgm fallback) still helps self-host but does not touch the production hang path. Actual prd EXPLAIN (workspace with 60k issues, keyword "search"): Index Scan using idx_issue_workspace on issue i Rows Removed by Filter: 59123 SubPlan 2 Bitmap Heap Scan on comment c Rows Removed by Index Recheck: 1928275 Heap Blocks: exact=48297 lossy=164696 Bitmap Index Scan on idx_comment_content_bigm rows=536761 Execution Time: 32345.002 ms Root cause: the correlated `EXISTS` over `comment` gets rewritten by the planner into a *hashed* subplan. Without a workspace_id filter in the subquery, that hashed set covers every comment in every workspace matching the LIKE — 536k rows for "search" — which spills work_mem into a lossy bitmap and rechecks 1.9M rows. Two-part fix: 1. Query rewrite. buildSearchQuery now emits `c.workspace_id = $wsParam` inside every comment subquery (WHERE phrase match, WHERE multi-term match, tier 7 rank, tier 8 rank, and the matched_comment_content COALESCE). The same $4 parameter is reused so Postgres treats it as a compile-time constant and pushes it into the hashed subplan's key, collapsing the set to this workspace's comments. 2. Supporting index (migration 135). New `idx_comment_workspace ON comment (workspace_id)`. Without it, the pushed-down filter still triggers a Seq Scan on `comment` because comment has no btree on workspace_id (only the FK constraint and composite (issue_id, ...) indexes). Locally verified against a repro that mirrors prd (5k issues in the target workspace, 100k comments in a sibling workspace all containing "search"): the plan drops from 60 ms (hashed global scan, no support index) to 3 ms (subplan uses idx_comment_workspace). Prd extrapolation from the same shape: 32.3 s → tens of milliseconds. Regression test TestBuildSearchQuery_CommentSubqueryWorkspaceScope asserts every `FROM comment c` in the generated SQL is followed by a `c.workspace_id = $4` filter, so a future refactor can't silently regress the plan back to the global-hash pathology. The statement_timeout guard from the earlier commit in this branch is kept — it still bounds the worst case if any future query shape regresses. Co-authored-by: multica-agent <github@multica.ai> * fix(search): address PR review — unwrap 135 + add project trigram indexes (MUL-4059) Both must-fix items from GPT-Boy's review: 1. Migration 135 unwrapped. The previous version buried `CREATE INDEX idx_comment_workspace` inside `DO $$ ... EXCEPTION WHEN OTHERS $$` — exactly the anti-pattern that caused MUL-4059 in the first place. `idx_comment_workspace` is not a CJK-bonus fallback; it is the critical support that makes the query rewrite land on an Index Scan instead of a Seq Scan. A silent failure (lock timeout, disk full, permission denied, schema drift) MUST abort the migration and fail deployment, not slip through as green. The unwrapped `CREATE INDEX IF NOT EXISTS` now propagates real errors to the migration runner, which aborts and does NOT record the version as applied. IF NOT EXISTS keeps idempotency for the operator-precreated case (`CREATE INDEX CONCURRENTLY ...` before running migrations on large prd tables). 2. Migration 134 now covers project search too. SearchProjects reads `LOWER(project.title)` and `LOWER(COALESCE(project.description, ''))`, and the pg_bigm equivalents in migration 039 silently no-op on pg_bigm-less images just like 032/033/036. Without the trigram fallback, project searches on self-host would still Seq Scan and hit the 3 s statement_timeout guard as a 503 — technically bounded but not actually fixed. Added `idx_project_title_trgm` and `idx_project_description_trgm`; the down migration drops them too. Also: fixed the search.go comment that said callers get a "standard 500" — they get a 503 with SQLSTATE-57014 mapping; the comment now matches reality. Verified: build clean, vet clean, existing search / timeout tests still green. Migration 135 dry-run (dropping the index, re-applying the unwrapped SQL under `ON_ERROR_STOP=1`) creates the index cleanly; a deliberate `CREATE INDEX` on a non-existent column now aborts psql with exit 3, confirming the migration runner would fail loudly on any real error. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |