mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-12 20:29:10 +02:00
70c856eb7c4886b13200987cfcc2bc1a19285607
340 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a23856bae3 |
MUL-1624 docs(email): clarify 888888 is opt-in; document SMTP option (#2666)
* docs(email): clarify 888888 is opt-in via MULTICA_DEV_VERIFICATION_CODE; document SMTP option in self-host docs The startup log line, .env.example, and SELF_HOSTING_ADVANCED.md still implied that the dev master code 888888 is auto-active whenever APP_ENV != "production". That has not been true since the master code was gated behind MULTICA_DEV_VERIFICATION_CODE — the fixed code is disabled by default and must be opted in explicitly. Also extend the docs site with the SMTP relay backend added in #1877: auth-setup, environment-variables, and self-host-quickstart now cover both Resend and SMTP options in EN and ZH. Co-authored-by: multica-agent <github@multica.ai> * docs(email): treat SMTP as an email backend in self-host docs and startup warning Address review feedback on #2666: - server: startup warning now fires only when both RESEND_API_KEY and SMTP_HOST are empty, since either one is a valid email backend. Otherwise the log mis-tells SMTP-only operators that verification codes go to stdout. - self-host-quickstart (EN/ZH): tell readers to fetch the verification code from whichever backend they configured (Resend or SMTP); fall back to stdout only when neither is configured. - auth-setup (EN/ZH): \"without Resend\" → \"without any email backend configured\" so the wording stays correct now that SMTP is a first-class option. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
75dc70686b |
fix(realtime): include actor_type in WS broadcast messages (#2668)
* fix(realtime): include actor_type in WebSocket broadcast messages
The WS broadcast message format was {type, payload, actor_id} but missing
actor_type. This meant the web UI could not distinguish agent from human
operations in real-time events at the top level.
While payload data for comments (author_type) and activities (entry.actor_type)
already included the type, the top-level message did not — causing the web UI
to display agent CLI operations as human operations when relying on the
broadcast actor identity.
Changes:
- server/cmd/server/listeners.go: add actor_type to all broadcast messages
- packages/core/types/events.ts: add actor_type to WSMessage interface
- packages/core/api/ws-client.ts: pass actor_type to event handlers
- packages/core/realtime/hooks.ts: update EventHandler type signature
- packages/core/realtime/provider.tsx: update EventHandler type signature
Fixes MUL-2260
Co-authored-by: multica-agent <github@multica.ai>
* test: add frame-shape unit test asserting actor_type in WS frames
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
a8ce0a8998 |
feat(cli): add 'multica issue cancel-task <task-id>' command (#2560)
Exposes the existing /api/tasks/{id}/cancel backend endpoint as a CLI
command. Combined with upstream #2107 (cancel running agent on
server-side task delete), this gives operators a way to interrupt a
runaway agent push-storm without resorting to admin-bypass on the
downstream PR.
Use cases:
- Titan / DevBot iterating beyond its boundary (e.g. push-skip loops)
- Codex turn that locked in tool-call spam
- Manual recovery when a long-running task needs to stop NOW
Symmetric with 'issue rerun': accepts the short ID prefix shown by
'issue runs', supports --issue scoping, and reuses resolveTaskRunID
for ambiguity handling.
Refs: PR#19 octo-server post-mortem (2026-05-13)
Co-authored-by: yujiawei <yujiawei@mininglamp.com>
|
||
|
|
58cc189dcd |
fix: honor quick-create squad mentions (#2586)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
add3135a42 |
feat(cli): add squad create/update/delete and member add/remove (#2574)
* feat(cli): add squad create/update/delete and member add/remove commands Implement missing squad management commands in the CLI: - squad create --name --leader [--description] - squad update <id> [--name] [--description] [--instructions] [--leader] [--avatar-url] - squad delete <id> - squad member add <squad-id> --member-id --type [--role] - squad member remove <squad-id> --member-id --type Also adds DeleteJSONWithBody to the API client for the member remove endpoint which uses DELETE with a JSON body. All commands support --output json for structured output. Co-authored-by: multica-agent <github@multica.ai> * fix(squad): add --output json to delete/member remove, return 404 on 0-row delete - squad delete: add --output json flag, emit {id, deleted} on success - squad member remove: add --output json flag, emit {squad_id, member_id, removed} - Backend RemoveSquadMember: change query to :execrows, check RowsAffected and return 404 'squad member not found' when 0 rows deleted Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
21b49eb59b |
fix(cli): resolve squad assignees in issue create/update/assign (MUL-2165) (#2551)
* fix(cli): resolve squad assignees in issue create/update/assign (MUL-2165) The CLI assignee resolver only searched workspace members and agents, so a quick-create input like "assign to <SquadName>" silently fell through to "Unrecognized assignee: <SquadName>" in the issue description — even though squads are first-class assignees server-side and the prompt's whole point was to route the work for the user. Extend resolveAssignee / resolveAssigneeByID to also fetch /api/squads, teach the actor display lookup to render squad names in table output, update the quick-create prompt and runtime-config command listing to mention `multica squad list` alongside members and agents, and lock in the new behavior with tests. Co-authored-by: multica-agent <github@multica.ai> * fix(cli): gate squad assignee resolution behind an allowed-kinds set (MUL-2165) The earlier MUL-2165 fix taught resolveAssignee / resolveAssigneeByID to also return (squad, ...), but those helpers are shared. Project lead and issue subscriber callers were still using them, and their target schemas reject squads — project.lead_type has a DB CHECK constraint (server/migrations/034_projects.up.sql:10) and the subscriber handler's isWorkspaceEntity switch only knows member/agent (server/internal/handler/handler.go:414). So `multica project create --lead "<SquadName>"` and `multica issue subscriber add --user "<SquadName>"` would resolve to (squad, ...) and surface as a 500/403 server-side instead of a clean CLI-side resolution error. Thread an assigneeKinds set through the resolver and the pickAssigneeFromFlags helper. Issue create/update/assign/list pass `issueAssigneeKinds` (all three); project lead and subscriber pass `memberOrAgentKinds`. The squads fetch is skipped entirely when not allowed, and the not-found / no-match error wording adapts to the allowed kinds so it never mentions a type the caller cannot use. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0345285b86 |
feat(quick-create): searchable actor picker + squad support (#2552)
* feat(quick-create): searchable actor picker + squad support (MUL-2163) - Replaces the flat agent dropdown in the "Create with agent" modal with a searchable PropertyPicker that lists Agents and Squads in separate sections, so users can filter by name and pick a squad as the creator. - Persists the selection as (lastActorType, lastActorId), removing the agent-only lastAgentId field on the quick-create store. - Adds squad_id to the quick-create API request and stamps it onto the task's QuickCreateContext. The handler resolves the squad to its leader agent (re-using validateAssigneePair) and the daemon claim path injects the squad-leader briefing when the task carries a squad hint, matching the behavior of issue-bound squad tasks. Co-authored-by: multica-agent <github@multica.ai> * fix(create-issue): forward squad picks across manual→agent switch Manual mode → agent mode previously only carried `agent_id`, so picking a squad and then flipping to agent silently fell back to the persisted actor / first visible agent and lost the user's choice. Carry `squad_id` on the same branch so the agent panel honors the squad pick. Adds a sibling test alongside the existing project-carry case. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
29082f7cfe |
feat: implement Squad feature MVP (#2505)
* feat: implement Squad feature MVP
- Add migration 084_squad: squad, squad_member, squad_activity_log tables
- Extend issue.assignee_type to support 'squad'
- Add sqlc queries for squad CRUD, member management, activity logs
- Add Go handler with full Squad API (CRUD, members, activity log)
- Register routes: /api/squads/*, /api/issues/{id}/squad-activity, /api/squad-activity
- Add Squad trigger logic:
- Assign Squad immediately triggers leader
- Every external comment on squad-assigned issue triggers leader
- Anti-loop: squad members' comments don't trigger leader
- Dedup: skip if leader already has pending task
- Add squad activity log API (方案 B) for leader no-op recording
- Add frontend TypeScript types (Squad, SquadMember, SquadActivityLog)
- Add protocol events: squad:created, squad:updated, squad:deleted
Co-authored-by: multica-agent <github@multica.ai>
* fix: address PR review blocking issues
1. validateAssigneePair now accepts 'squad' assignee_type
2. All squad endpoints validate workspace ownership via GetSquadInWorkspace
3. CreateSquadActivityLog restricted to squad leader agent only
4. AddSquadMember validates member exists in workspace
5. UpdateSquad auto-adds new leader to squad members
6. DeleteSquad transfers assigned issues to leader before deletion
7. IssueAssigneeType includes 'squad' in frontend types
Co-authored-by: multica-agent <github@multica.ai>
* feat: soft-delete squads via archive instead of hard delete
- Add migration 085: archived_at + archived_by columns on squad table
- ListSquads now excludes archived squads (ListAllSquads for admin)
- DeleteSquad → ArchiveSquad (sets archived_at, preserves all records)
- Transfer squad-assigned issues to leader before archiving
- SquadResponse includes archived_at/archived_by fields
- Frontend Squad type updated with nullable archived fields
Co-authored-by: multica-agent <github@multica.ai>
* feat: re-add Squads frontend entry (sidebar nav + pages)
Re-applies the frontend squad entry that was lost during a merge:
- Sidebar nav: Squads item with Users icon
- Paths: squads() and squadDetail() in workspace paths
- Routes: /squads and /squads/[id] pages
- Views: SquadsPage (list) and SquadDetailPage
- i18n: en 'Squads' / zh '小队'
- Reserved slug: 'squads'
Co-authored-by: multica-agent <github@multica.ai>
* fix: fix SquadsPage rendering - use PageHeader children pattern
PageHeader takes children, not title/actions props. The incorrect
usage caused a React rendering error. Now matches the pattern used
by autopilots and agents pages.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): add API client methods and package export for squads pages
* feat: complete Squad frontend - create dialog, member management, API methods
- Add CreateSquadModal with name/description/leader selection
- Register 'create-squad' in modal registry
- Wire 'New Squad' button to open the modal
- Add full API client methods: createSquad, updateSquad, deleteSquad,
addSquadMember, removeSquadMember
- Rewrite SquadDetailPage with:
- Member list showing resolved names
- Add/remove member UI
- Archive squad button
- Back navigation to squads list
Co-authored-by: multica-agent <github@multica.ai>
* feat: improve Squad UI - match create agent dialog style
- CreateSquadModal: proper Dialog with Header/Description/Footer,
agent picker with avatars, textarea for description
- SquadDetailPage: centered max-w-2xl layout, ActorAvatar for members,
Crown badge for leader, textarea for member description,
improved spacing and visual hierarchy
- Renamed 'role' field label to 'Description' in add member form
(describes the member's responsibilities in the squad)
Co-authored-by: multica-agent <github@multica.ai>
* feat(squad): add avatar, instructions; drop unique-name constraint
- 086: add squad.avatar_url
- 087: drop unique constraint on squad.name (squads with the same
name are legitimate across teams; uniqueness was an accidental
product constraint)
- 088: add squad.instructions (text, default '')
- UpdateSquad now COALESCEs avatar_url + instructions
- handler exposes Instructions in SquadResponse and accepts it in
UpdateSquad
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): assignable + mention target; trigger leader on assign
- assignee picker and @mention suggestion list squads alongside
agents and members; renders squad avatar/icon
- creating or updating an issue with assignee_type=squad enqueues
a task for the squad's current leader (mirrors agent-assignee
parking-lot rule: skip backlog only)
- workspace queries/hooks expose squads where needed for the
pickers
- locales updated for new picker copy
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): agent-style detail page with members + instructions tabs
- restructure squad detail page to mirror the agent detail page:
320px inspector (creator, leader, created/updated) + tabbed
pane (Members | Instructions) with dirty-guard AlertDialog
- inline name + avatar editing on the inspector
- inline description editor (modal textarea)
- members tab: leader + member picker with role descriptions,
swap leader, edit member roles, remove
- instructions tab: ContentEditor + Save (mirrors agent pattern)
- squads list shows the squad avatar/icon
- core types + api.updateSquad accept avatar_url + instructions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): inject leader briefing on claim (protocol + roster + instructions)
When a squad's leader agent claims a task on a squad-assigned issue,
append a system-level briefing to the agent's Instructions composed of:
1. Squad Operating Protocol — hard-coded rules: leader is a
coordinator, dispatch via @mention, stop after dispatching,
resume on re-trigger, do not work outside the roster.
2. Squad Roster — leader self-row plus one row per non-archived
member with a literal mention markdown string ([@Name](mention://
agent|member/<UUID>)) the leader can paste verbatim. Round-trips
through util.ParseMentions, enforced by a contract test.
3. Squad Instructions — the user-defined squad.instructions block,
omitted entirely when empty so we do not leave a dangling heading.
Non-leader members claiming the same issue receive no briefing.
Tests cover: full squad with mixed agent/human members, lone leader,
archived agents skipped, empty user instructions, mention round-trip,
and the leader/non-leader claim-handler gate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(squad): tell leader not to restate issue context in dispatch comment
After observing leaders padding their delegation comments with full
re-summaries of the issue body and prior discussion, make the
Operating Protocol explicit:
- assignees on Multica already have the full issue (title,
description, all comments, attachments) and workspace context;
- delegation comments should add only what cannot be inferred
(who is picked, why, extra constraints), aim for two or three
sentences;
- restating context is now an explicit hard rule violation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): unify leader evaluation into activity_log, add CLI command
- Squad member comments now trigger leader (only leader self-excluded)
- Replace squad_activity_log with activity_log (action: squad_leader_evaluated)
- Add CLI: multica squad activity <issue-id> <outcome> --reason
- Add API: POST /api/issues/{id}/squad-evaluated
- Update squad operating protocol to require evaluation recording
- Remove squad_activity_log table from schema and generated code
* feat(cli): add squad list, get, member list commands
* fix(squad): address review findings (P1+P2)
P1 fixes:
- Add 'squads' to reserved_slugs.json (source of truth)
- Add 'create-squad' to ModalType union
- Remove unused leaderOpen/selectedLeader in create-squad modal
- Replace literal JSX strings with i18n selectors (en + zh-Hans)
P2 fixes:
- Add 'squad' to mention regex (MentionRe)
- Fix human member lookup in squad briefing (use GetUser directly)
- Add squads routes to desktop app
- Add squad:created/updated/deleted to WSEventType + invalidation
- Reject archived squads as issue assignees
* fix(squad): restore zh-Hans key, publish activity event, invalidate issues on archive
- Restore create_project.title in zh-Hans modals.json (dropped by prior edit)
- Publish activity:created WS event after squad leader evaluation
- Invalidate issue queries on squad:deleted (archive transfers assignees)
- Add creator info to squad list cards
* fix(squad): realtime sync, rerun support, leader validation
- Use workspaceKeys.squads prefix for detail/member queries (realtime invalidation)
- Publish squad:updated after add/remove/role-change member mutations
- Support rerun for squad-assigned issues (targets leader agent)
- Reject assignment to squads whose leader is archived
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
623d29f276 |
feat(agents): one-click create from curated templates (Phase 1) (#2520)
* docs(agents): three-phase agent quick-create plan
Captures the full design for moving agent creation from manual form +
one-by-one skill attachment to a tiered experience:
- Phase 1 (this PR): one-click curated templates, AI-free.
- Phase 2 (next): AI-recommended skills via the existing quick-create
task mechanism — no new server-side LLM dependency.
- Phase 3 (later): AI creates the whole agent end-to-end, composing
Phase 2 with a new `multica agent create` CLI driver.
Documents the architectural decisions that keep all three phases on
existing infrastructure (no SSE, no server-side LLM SDK, no new WS
channels), the two soft blockers Phase 1 unlocks for later phases
(createSkillWithFiles TX composability + skill same-name dedupe), and
the scope decisions we explicitly opted out of (Anthropic plugin
marketplace, ClawHub UI affordances).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(skills): harden import against invalid UTF-8 and binary files
PG rejects two byte patterns in a TEXT column. Both crashed real skill
imports we hit while assembling the template catalog:
- Embedded NUL (0x00) -> SQLSTATE 22021. Already stripped by
sanitizeNullBytes, kept as-is.
- Other invalid UTF-8 (e.g. 0x91 — Windows-1252 smart quote in a skill
whose author saved prose from Word). sanitizeNullBytes now also runs
strings.ToValidUTF8 over the content so the second class no longer
takes the whole import down.
For non-text payloads (images, fonts, archives, compiled binaries),
sanitization isn't the right fix — agents never read those as text,
and the bytes can't survive a TEXT column at all. addFile now skips
them by extension before the per-bundle cap counters tick, logging
the skip so an unexpected drop leaves a breadcrumb.
Function name kept for compatibility with the many call sites; both
behaviours are strict supersets of the original.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(skills): split createSkillWithFiles for tx composition + add workspace find-or-create query
Two soft blockers cleared so create-from-template (next commit) can
fold N skill creates and the agent + binding writes into one outer
transaction:
1. createSkillWithFiles used to Begin/Commit its own tx. Caller
composition was impossible — N invocations meant N separate
transactions and no atomicity over the whole materialise step.
Pull the body into createSkillWithFilesInTx(ctx, qtx, input); the
original function becomes a thin wrapper that manages its own tx
for standalone callers. Existing call sites: zero behaviour change.
2. Add GetSkillByWorkspaceAndName sqlc query — workspace skill lookup
by name, anchored to UNIQUE(workspace_id, name) from migration
008. Lets the template materialiser implement find-or-create:
reuse the workspace's existing skill row when a template
references the same name, rather than crashing on the unique
constraint or polluting the workspace with `<name>-2` clones.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agents): agent template catalog + create-from-template endpoint
Server-side foundation for Phase 1 of the quick-create roadmap (see
docs/agent-quick-create-plan.md). Adds:
- server/internal/agenttmpl/ — embed-loaded catalog of curated agent
templates. Each template ships pre-written instructions plus a list
of skill URLs that get materialised into the workspace at create
time. Validation runs at startup (init() panics on a malformed
template) so a bad JSON ships as a deploy-time defect, not a
runtime 500. Slug must equal the filename basename so the URL
router is mirror-symmetric with the file layout.
- 11 starter templates covering Engineering / Writing / Building /
Testing (code-reviewer, frontend-builder, planner, docs-writer,
one-pager, html-slides, full-stack-engineer, …).
- Three new endpoints, all behind RequireWorkspaceMember:
GET /api/agent-templates — picker list (no instructions)
GET /api/agent-templates/:slug — detail with instructions
POST /api/agents/from-template — materialise + create
Create flow:
1. Auth + runtime authorization happen BEFORE the GitHub fan-out
so a 403 never wastes 20s of upstream fetches.
2. Pre-flight dedupe by cached_name reuses workspace skills
without an HTTP fetch — second create-from-the-same-template
drops from 20s to <100ms.
3. Parallel fetch (30s per-URL timeout) for the remaining skills.
4. Single transaction: every skill insert, the agent insert, and
the agent_skill bindings. On any upstream fetch failure the TX
rolls back and the API returns 422 with `failed_urls` so the
UI can name the bad source(s).
5. extra_skill_ids (user-supplied additions) are verified through
GetSkillInWorkspace per id before attach, so a malicious client
can't graft a skill from another workspace via UUID guessing.
- multica agent create --from-template <slug> CLI flag dispatches to
the new endpoint with a 60s ceiling, matching `multica skill import`.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agents): one-click create-from-template UI
Frontend half of Phase 1. CreateAgentDialog becomes a state machine
spanning four steps:
chooser → Start blank / From template cards
blank-form → existing manual form (post-chooser)
duplicate-form → existing form pre-filled from a duplicated agent
template-picker → grid of templates, click navigates to detail
template-detail → instructions + skill list preview + one-click Use
Picking a template never lands on the form: name auto-deduped against
existingAgentNames, runtime = first usable one, visibility = private.
Refinement happens on the agent detail page if needed. Same rationale
the doc spells out — templates exist precisely to skip configuration.
New components, all collapsible-by-default so quick-create stays fast:
- template-picker.tsx — categorised grid, lucide icons + semantic
accent tokens resolved through static maps so Tailwind's JIT picks
up every variant (dynamic class strings would silently miss).
- template-detail.tsx — instructions preview, skill list with cached
descriptions, Use CTA. Renders the failedURLs banner when a 422
fires — the only step that can trigger that response.
- instructions-editor.tsx — collapsed preview-card / expanded full
ContentEditor.
- skill-multi-select.tsx + skill-picker-list.tsx — shared multi-
select surface, also adopted by the existing skill-add-dialog.
- avatar-picker.tsx — agent avatar upload, mirrors the inspector's
visual language.
Schema-defended client (CLAUDE.md → API Response Compatibility): the
three new endpoints are wired through parseWithFallback with lenient
zod schemas. Desktop builds outlive any given server — a future
field rename / wrapping must not white-screen older installs.
listAgentTemplates accepts both the current bare array and a future
{templates: [...]} envelope. Coverage: 7 new schema-test cases in
schema.test.ts (null body, missing skills/instructions, malformed
create response, envelope migration).
Catalog + detail go through TanStack Query with staleTime: Infinity —
workspace-independent static data, no per-mount refetch.
Other:
- skill-add-dialog becomes a true multi-select (Confirm button +
checkbox list); attached skills are filtered out of the list.
- agents-page hands the freshly-created Agent back to the dialog so a
follow-up setAgentSkills can attach the form-selected skills.
- agent-overview-pane drops the mx-auto/max-w-2xl frame on config-
tab content; the wider dialog visual language reads better with
tabs filling the column.
- Every new UI string lives in both en/agents.json and
zh-Hans/agents.json under create_dialog.* / tab_body.skills.* —
locales/parity.test.ts blocks drift in CI.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ci): align skill import test + drop next-only lint suppression
- TestFetchFromSkillsSh_ResolvesRootLevelSkillMd now expects assets/logo.png
to be skipped; matches the new addFile binary-extension guard
(
|
||
|
|
454c8e3d1a |
feat: in-app preview for non-image attachments (#2528)
* feat(storage): add GetReader to Storage interface Adds a streaming read method to the Storage abstraction so callers can pull object bytes without forcing a full in-memory load. S3Storage wraps GetObject; LocalStorage opens the file with path-traversal and sidecar guards. Tests cover happy path, traversal rejection, sidecar rejection, and missing key. Used in the next commit by the attachment-preview proxy endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(server): add attachment preview proxy endpoint GET /api/attachments/{id}/content streams the raw bytes of a text-previewable attachment back to the client. Exists to (a) bypass CloudFront CORS, which is not configured on the CDN, and (b) bypass Content-Disposition: attachment which Chromium honors for iframe document loads. Media types (image/video/audio/pdf) intentionally do NOT go through this endpoint — clients render them directly from the signed CloudFront download_url, which is already served with Content-Disposition: inline. Hard cap: 2 MB. Larger files return 413. Anything outside the text whitelist returns 415. The whitelist (isTextPreviewable) mirrors the client-side dispatcher; the cross-reference comment in file.go flags the manual sync until a JSON SSOT generator lands. Response always uses Content-Type: text/plain; charset=utf-8 so a hostile HTML payload can't be re-interpreted as a document. The original MIME ships via X-Original-Content-Type for client dispatch. Cache-Control: no-store so revoked attachment access takes effect immediately on the next request. Tests cover happy path (md), extension fallback when content_type is generic, 415 (pdf), 413 (>2MB), foreign workspace (404 isolation), and the isTextPreviewable table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(core/api): add getAttachmentTextContent + preview error types Adds an ApiClient method that fetches the text body of an attachment via the new /api/attachments/{id}/content proxy. Two typed errors — PreviewTooLargeError (413) and PreviewUnsupportedError (415) — let the preview modal render specific fallbacks instead of a generic failure. Refactors the private fetch() into a shared fetchRaw() helper so the new method inherits the standard infra: auth headers, 401 → handleUnauthorized recovery, X-Request-ID, error logging, and the ApiError contract. The previous draft bypassed all of these by calling window.fetch directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(views/editor): add AttachmentPreviewModal + Eye entry points In-app preview for non-image attachments. An Eye icon now sits next to the existing Download button on file cards / readonly file cards / the standalone AttachmentList. Clicking it opens a full-screen modal that dispatches by content_type: pdf: <iframe src={download_url}> — Chromium PDFium video/*: <video controls src={download_url}> — native controls audio/*: <audio controls src={download_url}> — native controls md: <ReadonlyContent> — full markdown pipeline html: <iframe srcdoc sandbox=""> — fully restricted text: <code class="hljs"> — lowlight highlight Media types render directly from the signed CloudFront download_url (server marks them inline-disposition). Text types fetch through the new /api/attachments/{id}/content proxy via TanStack Query, wrapped in useAttachmentPreview() so each entry point owns its own modal state without depending on a global Provider mount. Modal sizing: max-w-6xl × min(90vh, 100vh - 2rem) — slightly larger than create-issue's max-w-4xl since PDF / video need room, but capped to viewport on small screens. Sub-renderers use h-full to follow the fixed modal height instead of viewport-relative units. Images are intentionally NOT touched — the existing ImageLightbox (extensions/image-view.tsx) already handles them correctly. The new modal would be churn without user-visible benefit. Adds i18n keys under attachment.* (en + zh-Hans) and registers Preview/Download/Upload in the conventions glossary so future translations stay consistent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(desktop): enable Chromium PDF viewer for attachment preview Adds webPreferences.plugins: true to the main BrowserWindow so the bundled Chromium PDFium plugin activates inside iframes — required for the attachment preview modal's PDF dispatch. Default is false in Electron; without it <iframe src=*.pdf> renders blank. Security trade-off, accepted intentionally and documented inline: 1. This window already runs with webSecurity: false + sandbox: false, so plugins: true does NOT meaningfully widen the renderer's attack surface beyond what is already accepted. 2. The only PDFs that reach an iframe here are signed CloudFront URLs we ourselves issued; user-supplied URLs are routed through setWindowOpenHandler → openExternalSafely and cannot land in this renderer. 3. Chromium's PDFium plugin is itself sandboxed and only handles application/pdf — no Flash/Java/other historical plugin surfaces. If we ever tighten webSecurity / sandbox, the follow-up is to host the PDF viewer in a dedicated BrowserView with plugins scoped to that view, keeping the main renderer plugin-free. Old desktop builds ship without the preview modal, so the Eye button never appears and PDF preview is gated by the same release — zero regression risk for users on stale clients. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
51aa924124 |
feat(chat): support renaming chat sessions inline (#2522)
Adds a pencil icon next to the trash icon on each session row in the chat
dropdown. Clicking it turns the title into an inline editable input:
Enter / blur saves, Escape cancels.
Server: new PATCH /api/chat/sessions/{id} handler that updates the title
via the existing `UpdateChatSessionTitle` sqlc query, broadcasts a new
`chat:session_updated` WS event so other tabs / devices stay in sync, and
rejects blank titles. Frontend mutation is optimistic with rollback,
matching the existing delete-session pattern.
MUL-2110
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
96695a79c5 |
feat(dashboard): workspace/project token + run-time dashboard MUL-1882 (#2462)
* feat(dashboard): workspace/project token + run-time dashboard
Add a `/{slug}/dashboard` page showing per-agent token spend and execution
time across the whole workspace, with an optional project filter.
Backend:
- Three new sqlc queries against task_usage + agent_task_queue: daily
usage, per-agent usage, per-agent total run-time. All optionally
scoped to a project via sqlc.narg('project_id'), reaching project
through the issue join.
- Handlers under /api/dashboard return the same wire shape the runtime
page already consumes (model preserved for client-side cost math).
Frontend: - Shared DashboardPage in packages/views/dashboard reusing KpiCard,
DailyCostChart, ActorAvatar, and estimateCost from the runtime page
so the visual style and pricing math stay in lock-step.
- Period selector (7/30/90d), project dropdown, four KPI tiles
(cost, tokens, run time, tasks), daily cost chart, and a combined
"cost + run time by agent" list.
- Routed in both web (app/[slug]/(dashboard)/dashboard) and desktop
(memory router); sidebar nav entry added under Workspace group.
Co-authored-by: multica-agent <github@multica.ai>
* fix(dashboard): drop stale project filter and stop double-counting tasks
Two issues caught in PR #2462 review:
1. Project filter held the previous selection's UUID across workspace
switches and project deletions: the dropdown gracefully showed
"All projects" (because the title lookup missed) while the three
dashboard queries kept forwarding the dead UUID, leaving the UI
looking like a full-workspace view but populated with empty
project-scoped data. Validate the picked UUID against the current
projects list before passing it to the queries.
2. The "by agent" table read its task count from the token rollup,
which is grouped per (agent, model). A single task that spans two
models lands twice and the agent's row reads e.g. "2 tasks" when
the real count is 1. Prefer `ListDashboardAgentRunTime`'s per-agent
distinct count when available; fall back to the token aggregate
only for agents with no terminal run yet (in-flight tasks).
Extract the merge into `mergeAgentDashboardRows` so the precedence
rules are unit-tested directly.
Co-authored-by: multica-agent <github@multica.ai>
* test(dashboard): allocate per-workspace issue.number explicitly
TestDashboardEndpoints creates two issues in the shared fixture
workspace. issue.number defaults to 0 (migration 020), and the table
carries UNIQUE (workspace_id, number), so the second insert raced the
first on the same default and failed in CI.
Allocate MAX(number) + 1 per insert so each row gets a fresh number
without stepping on rows other tests left behind in the same workspace.
Co-authored-by: multica-agent <github@multica.ai>
* feat(dashboard): rollup table + cron-driven aggregation for dashboard
Mirror the per-runtime rollup in `task_usage_daily` (migrations 073/077/082)
to remove the per-request raw aggregation the dashboard was doing.
Migration 084 adds:
- `task_usage_dashboard_daily` keyed on
(bucket_date, workspace_id, agent_id, project_id, model) — the
dimensions the dashboard actually queries, with project_id nullable
via UNIQUE NULLS NOT DISTINCT (PG15+) so "no-project" buckets
upsert cleanly.
- `task_usage_dashboard_rollup_state` watermark table.
- `task_usage_dashboard_dirty` invalidation queue.
- Triggers on agent_task_queue DELETE, task_usage DELETE, and
issue.project_id UPDATE — the cases the updated_at watermark can't
see. The project_id trigger re-attributes existing rollup rows when
a user moves an issue across projects.
- `rollup_task_usage_dashboard_daily_window(from, to)` —
idempotent recompute primitive (same shape as 077).
- `rollup_task_usage_dashboard_daily()` cron entry — own advisory
lock (4244) so it serialises independently of the runtime rollup.
- `task_usage_dashboard_rollup_lag_seconds()` health helper.
Sqlc queries `ListDashboardUsageDailyRollup` /
`ListDashboardUsageByAgentRollup` read from the new table; the handler
dispatches between rollup and raw on a separate
`UseDailyRollupForDashboard` config flag
(`USAGE_DASHBOARD_ROLLUP_ENABLED` env). Same fail-safe default (false →
raw) so operators can roll out independently of the per-runtime flag.
Bucket date is UTC (the dashboard aggregates across runtimes that may
sit in different tzs; there's no single correct local boundary).
Adds `cmd/backfill_task_usage_dashboard_daily` mirroring the existing
per-runtime backfill — operator runs it once before flipping the flag.
Tests: - TestDashboardEndpoints now also exercises the rollup read path
(raw vs. rollup, same project-scoped totals).
- TestDashboardRollupReattributesOnProjectChange verifies the
issue.project_id trigger enqueues both old + new buckets and the
next rollup tick zeroes the old project + populates the new one.
Co-authored-by: multica-agent <github@multica.ai>
* fix(dashboard-rollup): close two invalidation gaps
Two leak paths missed by migration 084 review:
1. Issue cascade DELETE — the atq BEFORE DELETE trigger runs AFTER the
issue row is gone, so `LEFT JOIN issue` returns NULL project_id and
the original-project bucket never gets cleared (issue 077 calls this
out for the runtime rollup but didn't need to act on it). Adds an
`issue BEFORE DELETE` trigger that enqueues using OLD.project_id
while the issue row is still readable.
2. `LinkTaskToIssue` (quick-create task attaching to a real issue post-
completion) UPDATEs `agent_task_queue.issue_id` from NULL to a real
id. Migration 084 only watched DELETE on atq, so usage already
rolled up under the no-project bucket stayed attributed to NULL
forever. Extends the atq trigger to fire on UPDATE OF issue_id too,
enqueueing both OLD (NULL project) and NEW (linked issue's project).
Tests: - TestDashboardRollupClearsOnIssueDelete asserts rollup row drops to
zero after issue delete + rollup tick.
- TestDashboardRollupReattributesOnLinkTaskToIssue verifies tokens
move from the NULL bucket to the project bucket after the UPDATE.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
caeb146bac |
feat(github): GitHub App integration for PR ↔ issue linking (#1817)
* feat(github): GitHub App backend for PR ↔ issue linking - New tables: github_installation (workspace ↔ App install), github_pull_request (mirrored PR state), issue_pull_request (M:N link). - Webhook handler verifies HMAC-SHA256, upserts PR rows, parses issue identifiers from PR title/body/branch and auto-links them. Merging a linked PR moves the issue to done. - Connect/setup endpoints power the zero-config "Connect GitHub" install flow; state token is HMAC-signed so the setup callback can recover the workspace. - Workspace-scoped admin routes for listing/disconnecting installations, plus a per-issue `pull-requests` list endpoint. Co-authored-by: multica-agent <github@multica.ai> * feat(github): UI for connecting GitHub and viewing linked PRs - Settings → Integrations: new tab with Connect GitHub / installations list / disconnect, gated on the deployment having the App configured. - Issue detail sidebar: Pull requests section showing linked PR title, repo, state (open/draft/merged/closed), and author, with deep link to GitHub. - Real-time refresh: github_installation:* and pull_request:* events invalidate the matching TanStack Query caches. Co-authored-by: multica-agent <github@multica.ai> * fix(github): address review — null actor, role gating, configured guard, scoped uninstall broadcast - listeners: use optionalUUID(e.ActorID) so the system actor on the github-driven issue:updated event no longer panics activity / notification listeners; merged-PR → issue done now produces a status_changed activity and inbox entry. - IntegrationsTab: gate the admin-only installations query on canManage so members no longer hit /github/installations 403; the configured/not-configured copy is also scoped to admins. - backend: introduce isGitHubConfigured() requiring both GITHUB_APP_SLUG and GITHUB_WEBHOOK_SECRET, and surface that single flag from list-installations + connect endpoints so the frontend Connect button stays disabled until both are set. - DeleteGitHubInstallationByInstallationID now RETURNs workspace_id; webhook handler publishes github_installation:deleted scoped to the right workspace so already-open Settings tabs invalidate in real time. ErrNoRows on a re-fired delete short-circuits cleanly. - tests: focused webhook integration coverage (auto-link + merge → done, cancelled preservation, uninstall returns workspace). Co-authored-by: multica-agent <github@multica.ai> * fix(github): i18n the new GitHub UI strings to satisfy lint CI flagged every literal string in the Integrations tab, the Pull requests sidebar section, and the per-PR row label. Move them through useT() and add the matching `integrations.*` block to settings.json (en / zh-Hans) plus `detail.section_pull_requests` / `detail.pull_request_state_*` / loading + empty copy under `issues.json`. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
046e4b1efa |
fix(execenv): switch every provider's Windows reply template to --content-file (#2411)
Three user reports converge on the same Windows-shell encoding bug: - #2198 / #2236 — Chinese, Codex on Win11. Comments / descriptions generated by the agent arrive as `?`. - #2376 — Cyrillic, non-Codex agent ("Ops Lead") on Win11 Desktop. Title preserved (argv → CreateProcessW UTF-16), description / agent reply garbled (stdin → shell-codepage re-encoding). woodcoal's independent diagnosis on #2198 confirms the root cause: Windows PowerShell 5.1's `$OutputEncoding` defaults to ASCIIEncoding when piping to a native command, so non-ASCII bytes are silently replaced with `?` before they reach `multica.exe`. The CLI's stdin parsing is fine; the bytes are corrupted upstream, in the agent's shell layer. This PR ships the fix that supersedes the codex-only attempt in PR #2265 (which is closed in favour of this one): ## CLI Add `--content-file <path>` to `multica issue comment add` and `--description-file <path>` to `multica issue {create,update}`. The CLI reads bytes off disk via `os.ReadFile` and skips the shell entirely; UTF-8 survives end-to-end regardless of `$OutputEncoding` or `chcp`. The three input modes (`--content`, `--content-stdin`, `--content-file`) are mutually exclusive. ## Runtime config `buildMetaSkillContent`'s Available Commands section is rewritten as a neutral three-mode menu. The previous unconditional "MUST pipe via stdin" / `--description-stdin` mandate (over-spread from #1795 / #1851's Codex-multi-line fix) is gone for non-Codex providers; the strong directive now lives only in the Codex-Specific section, which branches on host: - Codex / Linux+macOS: `--content-stdin` + HEREDOC (preserves MUL-1467 fix against codex's literal `\n` habit). - Codex / Windows: `--content-file` (PowerShell ASCII pipe is the exact bug we're patching). ## Per-turn reply template `BuildCommentReplyInstructions` now takes a provider arg and branches provider × OS: - Windows + any provider → `--content-file` (the bug is shell-layer, not provider-layer; #2376 shows non-Codex agents on Windows also hit it). All providers write a UTF-8 file with their file-write tool and post via `--content-file ./reply.md`. - Linux/macOS + Codex → stdin/HEREDOC (MUL-1467 protection). - Linux/macOS + non-Codex → lightweight pre-#1795 inline `--content "..."`. The CLI server-side decodes `\n`, so escaped multi-line works; the agent retains stdin / file as escape hatches for richer formatting. `BuildPrompt` and `buildCommentPrompt` gain a `provider` arg; `daemon.runTask` already has it in scope. ## Tests - `TestResolveTextFlag` — file-source verbatim with non-ASCII (`标题 / Заголовок / 中文段落`), missing-file error, empty-file rejection, three-way mutual exclusion. - `TestInjectRuntimeConfigAvailableCommandsIsNeutral` — every non-Codex provider × {linux, darwin, windows} pins the three-mode menu present + over-spread "MUST stdin" substrings absent. - `TestInjectRuntimeConfigCodexLinuxEmphasizesStdin` + `TestInjectRuntimeConfigCodexWindowsUsesContentFile` — Codex section's per-OS branch. - `TestBuildCommentReplyInstructionsCodexLinux` + `TestBuildCommentReplyInstructionsNonCodexLinux` + `TestBuildCommentReplyInstructionsWindowsUsesContentFile` — the reply-template provider × OS matrix. - `TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin` — end-to-end AGENTS.md / CLAUDE.md on Windows has no prescriptive stdin directive, for claude / codex / opencode. `go test ./...` and `go vet ./...` clean. Closes #2198, #2236, #2376. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fb026f2607 |
fix(daemon): suppress git console windows on Windows (#2358)
* fix(daemon): suppress git console windows on Windows Apply the same HideConsoleWindow pattern used for agent processes (PR #1474) to all git commands spawned by the daemon's repo-cache, execenv, and GC packages. Each exec.Command now calls util.HideConsoleWindow(cmd) which sets CREATE_NEW_CONSOLE + HideWindow so grandchildren inherit a hidden console instead of flashing visible console windows. Closes #2357 Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com> * refactor: use EnsureHiddenConsole at daemon startup Replace per-site HideConsoleWindow(cmd) calls with a single EnsureHiddenConsole() invoked once at daemon startup. The daemon now owns a hidden console that every child process (git, cmd /c mklink, etc.) inherits automatically, eliminating the need for per-call SysProcAttr configuration. This also covers the previously missed exec.Command in codex_home_link_windows.go (cmd /c mklink) which never had a HideConsoleWindow call. Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com> --------- Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com> Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com> |
||
|
|
d6349c16ec |
feat(runtime): per-runtime timezone for token-usage aggregation (MUL-1950) (#2394)
* feat: per-runtime timezone for token usage aggregation The runtime token-usage charts (daily and hourly tabs on the runtime-detail page) bucketed every event by the Postgres session timezone, which is UTC in production. For an operator in UTC+8 that meant a Tuesday afternoon's tasks landed in Tuesday early-morning's bar — the chart was always one off. Fix: store an IANA timezone on agent_runtime and aggregate under it. * migrations 081 / 082 add agent_runtime.timezone (TEXT NOT NULL DEFAULT 'UTC') and rebuild the rollup pipeline (window function and both trigger functions) to compute bucket_date with AT TIME ZONE rt.timezone instead of bare DATE(). * No historical backfill — task_usage_daily rows already on disk keep their UTC bucket_date; only future writes / re-touches recompute under the new tz. (Product call from MUL-1950: 'guarantee future correctness'.) * runtime_usage.sql gains a @tz parameter on ListRuntimeUsage and GetRuntimeUsageByHour and threads tz through GetRuntimeTaskHourly Activity. ListRuntimeUsageDaily reads bucket_date as-is since the rollup already wrote it in tz. * parseSinceParamInTZ replaces the raw N×24h cutoff with start-of- day-N in the runtime's tz so 'last 7 days' lines up with bucket boundaries. * Daemon registration sends the host's IANA tz (TZ env, then time.Local), and UpsertAgentRuntime preserves any user override via a CASE-on-existing-value pattern so a daemon reconnect can't silently revert the operator's setting. * New PATCH /api/runtimes/:id endpoint (UpdateAgentRuntime) lets the runtime detail page edit the tz; the editor seeds with the browser tz on first interaction. Refs: MUL-1950 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix: harden runtime timezone rollups Co-authored-by: multica-agent <github@multica.ai> * fix: address runtime timezone review nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica.ai> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Eve <eve@multica-ai.local> |
||
|
|
b26f850d4e |
feat(agents): gate private-agent surfaces with allowed_principals predicate (#2359)
* feat(agents): gate private-agent surfaces with allowed_principals predicate
Tighten chat/@-mention, history, edit, and delete entry points so private
agents are only reachable by their owner or workspace owner/admin. Agent-to-
agent traffic still bypasses the gate so A2A collaboration keeps working.
- New canAccessPrivateAgent predicate in handler/agent_access.go; used by
comment.enqueueMentionedAgentTasks (replacing the inline check), GetAgent,
ListAgents (filter), ListAgentTasks, GetWorkspaceAgentRunCounts /
Activity30d / TaskSnapshot (workspace-wide aggregations no longer leak
private-agent existence + counts), chat.CreateChatSession,
chat.SendChatMessage (re-checks on every send so role changes can't leave
a stale session as a back-door), and autopilot.shouldSkipDispatch
(caller = autopilot creator).
- allowed_principals is computed inline as {agent.owner_id} ∪ workspace
owner/admin members. No new table — manual config is intentionally not
exposed in v1; the predicate is the extension seam.
- Front-end agent detail page distinguishes 403 (private agent the caller
can't access) from 404 (deleted/missing) and renders a "no access"
placeholder with a back-to-agents button.
- Go tests cover the pure predicate matrix + the four protected surfaces;
vitest passes for the affected views.
Co-authored-by: multica-agent <github@multica.ai>
* feat(agents): gate issue assignment with the private-agent predicate
Refactor validateAssigneePair to call the shared canAccessPrivateAgent
helper. This closes the back door where a plain member could assign a
private agent to an issue and let normal task dispatch run it, side-
stepping the chat / @-mention gate. Agent callers (X-Agent-ID) bypass
so A2A delegation onto a private assignee still works.
Add an integration test covering all three callers (workspace owner,
agent owner, plain member).
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): close three private-agent gate bypasses found in PR review
1. X-Agent-ID forgery (resolveActor): require X-Task-ID alongside
X-Agent-ID before trusting the agent identity. Without this a plain
workspace member could set X-Agent-ID to any visible agent UUID and
short-circuit the gate to "actor=agent, allow". Daemons already
pair the two headers, so legitimate A2A traffic is unaffected.
2. Chat history read path (chat.go): GetChatSession / ListChatMessages /
GetPendingChatTask / MarkChatSessionRead now go through a new
gateChatSessionForUser helper that re-applies canAccessPrivateAgent
after the ownership check, so a session creator whose role was later
downgraded loses transcript access. ListChatSessions and
ListPendingChatTasks filter their result sets by the same predicate.
3. Cross-workspace @mention (comment.enqueueMentionedAgentTasks):
resolve the mentioned agent via GetAgentInWorkspace scoped to the
issue's workspace so a UUID belonging to a different workspace's
private agent can't slip past the gate (the gate was being applied
against the current workspace's role table, which is the wrong
one).
Regression tests cover each bypass, plus an update to the resolveActor
unit test to reflect the new "X-Agent-ID without X-Task-ID falls back
to member" contract.
Co-authored-by: multica-agent <github@multica.ai>
* test(handler): seed X-Task-ID alongside X-Agent-ID in existing agent-caller tests
After tightening resolveActor to require both headers (X-Agent-ID +
X-Task-ID) for the "agent" actor identity, three existing tests that
set only X-Agent-ID started failing because their requests now resolve
to "member" instead of "agent". Add createHandlerTestTaskForAgent
helper and seed a task per agent-caller assertion. Also patch
TestAgentExplicitMentionStillTriggers — it still passed only because
the @mention path doesn't care about author type for member callers,
but the test claims to exercise the agent path, so make it faithful.
Co-authored-by: multica-agent <github@multica.ai>
* test(handler): finish X-Task-ID seeding + fix cross-workspace mention test schema
The previous CI run still failed in two places:
1. server/cmd/server integration tests — postCommentAsAgent → authRequestWithAgent
only set X-Agent-ID, so resolveActor downgraded the request to "member"
and the on_comment chain produced the wrong task counts. Fix:
authRequestWithAgent now also sets X-Task-ID, fetched or seeded by a new
ensureAgentTask(agentID) helper.
2. TestMentionAgent_RejectsCrossWorkspaceAgentUUID's hand-crafted comment
INSERT was missing comment.workspace_id, which migration 025 made
NOT NULL. Pass testWorkspaceID into the seed row.
Build + vet clean locally; both packages compile.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
39e57b870f |
fix(cli): allow --mode run_only on autopilot create/update (#2360)
* fix(cli): allow --mode run_only on autopilot create/update The autopilot run_only dispatch path is wired end-to-end (handler accepts the mode, AutopilotService.dispatchRunOnly enqueues a task with AutopilotRunID, daemon resolves workspace via autopilot_run -> autopilot in ClaimTaskByRuntime and TaskService.ResolveTaskWorkspaceID). The CLI guard was added before those fixes landed and never removed. Drop the CLI rejection on both create and update so callers can pick the same modes the API and UI already support, and remove the stale "unstable" callout from the autopilots docs. Closes multica-ai/multica#2347 Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): advertise autopilot run_only in agent runtime instructions The runtime config injected into AGENTS.md / CLAUDE.md only listed `--mode create_issue` for autopilot create and didn't expose `--mode` on update at all. So even after the CLI guard was lifted, agents reading their harness instructions would still believe create_issue was the only choice — undermining the "agents operate the same surface as humans" intent. Update both lines to advertise create_issue|run_only on create and on update, and add an InjectRuntimeConfig assertion so the runtime prompt can't drift away from the CLI surface again. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
003dfd9b4b |
feat(quick-create): add project picker that remembers last pick (#2321)
* feat(quick-create): add project picker that remembers last pick Quick-create users targeting one project repeatedly had to restate "in project X" in every prompt. The modal now exposes a project picker beside the agent picker, persists the selection per-workspace, and pins the agent's `multica issue create` invocation to that project so the prompt text doesn't have to. The picked project also flows to the daemon as ProjectID/ProjectTitle and its github_repo resources override the workspace repo fallback — same treatment issue-bound tasks already get. Co-authored-by: multica-agent <github@multica.ai> * fix(quick-create): move project picker into property pill row Reviewer feedback: the picker felt out of place wedged next to the agent header. Move it into a property toolbar row above the footer, reusing the shared `ProjectPicker` + `PillButton` so its placement and styling line up exactly with the manual create panel. This also drops the bespoke dropdown / aria / label strings that were only needed while the picker rendered inline beside "Created by". Co-authored-by: multica-agent <github@multica.ai> * fix(quick-create): clear stale persisted project + carry across mode switch Two review-blocking bugs in PR #2321: 1. The stale-id sweep in AgentCreatePanel only fired when projects.length > 0 and only cleared local state, leaving lastProjectId pointing at a deleted project. The next open re-seeded the dead UUID and submit hit the server's `project not found` rejection. Gate on the query's `isSuccess` so we can tell "loading" apart from "loaded as empty", and clear both local state and the persisted preference when the selection isn't in the resolved list. 2. ManualCreatePanel's switchToAgent dropped the picked project from the carry payload, so flipping manual → agent silently fell back to the agent panel's own lastProjectId — potentially routing the issue to a different project than the one shown in manual mode. Forward project_id alongside prompt / agent_id, and add a regression test. Co-authored-by: multica-agent <github@multica.ai> * test(quick-create): pass new isExpanded props in stale-project tests Main got an expand button on AgentCreatePanel via #2320 while this branch was open, adding `isExpanded` / `setIsExpanded` to the panel's required props. The two new stale-project tests still passed `{ onClose }` only, which CI's typecheck (run on the main+branch merge) caught while my local run did not. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3f20999597 |
refactor(timeline): drop server-side comment + timeline pagination (#2322)
* refactor(timeline): drop server-side comment + timeline pagination (MUL-1929) The cursor-paginated /timeline and /comments endpoints were sized for a problem the data shape doesn't have: prod p99 is ~30 comments per issue and the all-time max is ~1.1k. Time-based pagination also splits reply threads across page boundaries (orphan replies), which the frontend was papering over with an "orphan rescue" that promoted disconnected replies to top-level — confusing UX with no real benefit. Replace both endpoints with a single full-issue fetch, capped server-side at 2000 rows as a defensive safety net (never hit in practice). Server - /api/issues/:id/timeline now returns a flat ASC TimelineEntry[] (matches the legacy desktop contract — older Multica.app builds keep working because the wrapped TimelineResponse + cursors are gone, and the raw array shape was always what they consumed). - /api/issues/:id/comments drops limit/offset; only ?since is honoured for the CLI agent-polling flow. - Drop ListCommentsBefore/After/Latest, ListActivitiesBefore/After/Latest and the timelineCursor encoding. - Replace with ListCommentsForIssue / ListCommentsSinceForIssue / ListActivitiesForIssue (capped by argument). CLI - multica issue comment list drops --limit / --offset and the X-Total-Count reporting; --since is preserved for incremental polling. Frontend - Replace useInfiniteQuery with useQuery in useIssueTimeline; drop fetchOlder/Newer, jumpToLatest, isAtLatest, newEntriesBelowCount. - Remove timeline-cache helpers (mapAllEntries / filterAllEntries / prependToLatestPage) and the TimelinePage / TimelinePageParam types. - WS event handlers update the single flat-array cache directly. - Drop the orphan-reply rescue in issue-detail — every reply's parent is now guaranteed to be in the same array. - Strip the "show older / show newer / jump to latest" buttons and their i18n strings. Co-authored-by: multica-agent <github@multica.ai> * fix(timeline): address review feedback on pagination removal Three issues caught in PR #2322 review: 1. /timeline broke for stale clients between #2128 and this PR. They send ?limit/?before/?after/?around and parse with the wrapped TimelinePageSchema; the new flat-array response was failing schema validation and falling back to an empty timeline. Restore the wrapped shape on those query params (DESC entries, null cursors, has_more_*=false), keeping the flat ASC array for bare requests. Around-mode now also fills target_index from the merged slice so legacy clients can still scroll-to-anchor without a follow-up. 2. The agent prompts in runtime_config.go and prompt.go still told agents that `multica issue comment list` accepts --limit/--offset and to use `--limit 30` on truncated output. With those flags removed in this PR, new agent runs would hit "unknown flag" or skip context. Update the prompt copy to "returns all comments, capped at 2000; --since for incremental polling". 3. useCreateComment's onSuccess was a bare append to the timeline cache with no id-dedupe, so a fast comment:created WS event firing before onSuccess produced a transient duplicate. Restore the id guard the old prependToLatestPage helper used to provide. Adds two new boundary tests: - TestListTimeline_LegacyWrappedShape_OnPaginationParams - TestListTimeline_LegacyWrappedShape_AroundFillsTargetIndex Co-authored-by: multica-agent <github@multica.ai> * test(handler): fix timeline test assertions for handler-package isolation The TestListTimeline_* assertions assumed CreateIssue would seed an "issue_created" activity_log row, but the activity listener that publishes those rows is registered in cmd/server/main.go — handler-package tests don't wire it up. CI saw 5 entries (3 comments + 2 activities) where the test expected ≥6. Drop the auto-activity assumption: assert exactly 5 entries in TestListTimeline_MergesCommentsAndActivities, and tighten TestListTimeline_EmptyIssue to assert a fully-empty timeline. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9ded462ecc |
feat(inbox): auto-archive stale task_failed rows on terminal status (#2319)
When an issue progresses to in_review / done / cancelled, archive any pre-existing task_failed inbox rows for that issue across all member recipients and emit inbox:batch-archived per recipient so connected clients self-heal. Reuses the existing archived column rather than introducing a parallel dismissed flag; the activity log preserves the full failure history for audit independently of the inbox surface. Closes #2291. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a2dd80d4f6 |
feat(autopilot): skip dispatch when assignee runtime is offline (MUL-1899) (#2311)
* feat(autopilot): skip dispatch when assignee runtime is offline (MUL-1899) Prevents scheduled autopilots from accumulating doomed tasks against offline / archived / unbound agents. Before this change, a paused laptop or crashed daemon would let a 5-minute-cron autopilot pile up thousands of queued agent_task_queue rows that no runtime would ever drain — this is the dominant source of the 89k stuck-task backlog flagged in MUL-1899. DispatchAutopilot now performs a pre-flight admission check on the assignee agent's runtime status. If the runtime is not 'online' (or the agent is archived / has no runtime bound / has no assignee), the run is recorded as 'skipped' with a failure_reason and no task is enqueued. Skipped runs still emit autopilot:run.done so the UI / activity feed reflect that the trigger fired and was evaluated. Skipped runs are deliberately NOT counted toward the failure-ratio auto-pause: a user who closes their laptop overnight should not have their autopilot paused. Sustained server-side failures keep their existing pause path via the failure monitor. Tests: added an integration test that creates an offline runtime and asserts DispatchAutopilot records a skipped run with no task enqueued. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * feat(scheduler): expire stale queued tasks via TTL sweeper (MUL-1899) Companion to the dispatch-time admission gate added in this PR. The admission gate prevents *new* tasks from being enqueued against an offline runtime, but it does not drain the historical backlog (~89k stuck queued rows observed at MUL-1899 baseline) and does not help when a runtime goes offline *after* a task has already been queued. This adds a passive TTL sweeper: - New SQL query `ExpireStaleQueuedTasks` transitions queued tasks older than the TTL to status='failed' with failure_reason='queued_expired' and a clear error message. - Sweep is capped per tick (`queuedExpireBatchSize`, default 500) via a CTE+LIMIT so that draining a large backlog cannot monopolise the DB on a single tick. At 30s ticks the worst case is 60k rows/hour. - Wired into the existing 30s `runRuntimeSweeper` loop alongside `sweepStaleTasks` and reuses `taskSvc.HandleFailedTasks` so the expired tasks broadcast `task:failed` events, reconcile agent status, and roll back any in-progress issues — same lifecycle as any other failed task. - Default TTL = 2h. Conservatively above any reasonable "queued behind a long-running task" window (default agent timeout is 2h, sweeper runs every 30s) so legitimate work isn't expired. - Integration tests cover the happy path (stale → expired, fresh → left alone, correct status/reason/error) and the per-tick batch cap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix(autopilot): address review blockers from PR #2311 (MUL-1899) GPT-Boy review of the offline-runtime + queued-TTL PR flagged four blockers; this commit addresses them all. 1. Restore the 'skipped' autopilot_run status in the DB constraint. Migration 043 had removed 'skipped' along with the now-defunct concurrency_policy feature, so the new admission gate's INSERT of status='skipped' violated `autopilot_run_status_check` and broke `TestAutopilotDispatchSkipsWhenRuntimeOffline` in CI. New migration 079 re-adds 'skipped' to the CHECK list. The down migration migrates skipped → failed before re-tightening, mirror- ing what 043 did for the original removal. 2. Make `ExpireStaleQueuedTasks` race-safe. The CTE-then-UPDATE pattern could clobber a task that the daemon claimed between victim selection and the outer update. Two guards added: - `FOR UPDATE SKIP LOCKED` in the CTE so we never wait on a row that's currently being claimed (and never block the claim path either). - The outer UPDATE now re-checks `t.status = 'queued'` AND the TTL predicate so even if a row's lock is released after a successful claim, we cannot transition a now-dispatched/ running task to 'failed'. 3. Add a partial index for the queued-TTL sweeper. `idx_agent_task_queue_queued_created_at` on `created_at WHERE status = 'queued'` — keeps the 30s sweep query (status=queued AND created_at < ... ORDER BY created_at LIMIT 500) cheap even when historical terminal rows accumulate (~89k+ at MUL-1899 baseline). The partial predicate keeps the index tiny because only in-flight rows live in 'queued'. 4. Fix the failure-monitor denominator. `SelectAutopilotsExceedingFailureThreshold` had been counting 'skipped' toward total runs, which would have diluted the failure ratio: a 100%-failing autopilot could mask itself behind a wall of admission skips. With 'skipped' restored as a real status, the auto-pause monitor must explicitly exclude it from BOTH numerator and denominator — admission skips are neither a success nor a failure. Verified: `go test ./cmd/server/... ./internal/service/...` passes (including TestAutopilotDispatchSkipsWhenRuntimeOffline, TestExpireStaleQueuedTasks, TestExpireStaleQueuedTasksRespectsBatch Limit). `go build ./... && go vet ./...` clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix(migrations): split queued-task TTL index into concurrent migration Per PR #2311 review: agent_task_queue is a hot table, so building the new partial index with plain CREATE INDEX inside migration 079 would hold ACCESS EXCLUSIVE on the queue and block dispatch during deploy. The migration runner does not allow CONCURRENTLY to share a file with other statements (documented in 068), so split the index into its own single-statement file 080 — matching the existing pattern in 035 / 067 / 074 / 075 / 078. Migration 079 keeps the autopilot_run constraint change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6d9ebb0fdd |
fix(daemon): unblock issues stuck on a poisoned-image agent session (#2314)
* fix(daemon): treat upstream API 400 invalid_request_error as poisoned session A markdown-linked image in an issue description that the agent downloads as a tiny CDN auth-error file and Read's as a PNG poisons the conversation: the LLM API rejects the bad image with 400 invalid_request_error, the session_id is pinned mid-flight, and every follow-up task on the issue (comment-trigger, auto-retry) resumes the same poisoned conversation and hits the same 400 — the issue can no longer be executed even after the description is cleaned up. Mirror the existing fallback-output classifier on the error side: detect "API Error: ... 400 ... invalid_request_error" in the agent error string, persist failure_reason='api_invalid_request', and add it to the GetLastTaskSession exclusion list so the next task starts a fresh session that re-reads the (now-clean) description. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): unblock issues already poisoned by API 400 invalid_request_error The forward-only classifier from the previous commit only tags new failures. Issues like MUL-1918 already have multiple failed-task rows whose failure_reason is the pre-fix default 'agent_error', and GetLastTaskSession falls back to those legacy rows on the next claim — so deploying the classifier alone leaves existing poisoned issues stuck (GPT-Boy review on PR #2314). Two complementary changes: - Migration 079 backfills failure_reason='api_invalid_request' on every pre-existing 'agent_error' row whose error text matches the canonical Anthropic 400 invalid_request_error shape. Keeps observability consistent (multica issue runs / UI now report the right reason). - GetLastTaskSession adds a defensive ILIKE clause on error text. Closes the deploy-window gap where the old binary could write a new 'agent_error' row between the migration running and the new code taking over, and protects against future error-format variants the daemon classifier might miss. Plus regression tests covering the legacy + new coexistence case GPT-Boy flagged, and a guard rail asserting benign 'agent_error' failures (timeouts, tool errors) still resume their session. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ce00e05169 |
Add canonical PostHog core metrics events (#2302)
* Add canonical PostHog core metrics events Co-authored-by: multica-agent <github@multica.ai> * Address analytics review feedback Co-authored-by: multica-agent <github@multica.ai> * Tighten analytics review follow-ups Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3b3be9d7bd |
feat(comments): resolve threads with collapsible bar (MUL-1895) (#2300)
* feat(comments): resolve threads with collapsible bar (MUL-1895)
Adds a Linear-style resolve action on comment thread roots. Resolved
threads collapse to a single "N resolved comments from X" bar in the
activity feed; clicking expands the thread inline (per-session, not
persisted). Replying inside a resolved thread auto-unresolves it.
Backend
- migration 069: resolved_at, resolved_by_type, resolved_by_id on comment
- sqlc ResolveComment / UnresolveComment queries (idempotent via COALESCE)
- POST/DELETE /api/comments/{id}/resolve handlers, root-only validation
- CreateComment auto-clears resolved_at when a reply lands in a resolved
thread, publishing comment:unresolved
- comment:resolved / comment:unresolved events; CommentResponse and
TimelineEntry both surface the new fields
Frontend
- Comment + TimelineEntry types extended; payloads typed; WS sync wired
- useResolveComment optimistic mutation with rollback
- ResolvedThreadBar component for the collapsed view
- Resolve / Unresolve menu items on root comments; Collapse strip on the
expanded resolved card
- en + zh-Hans locale strings
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): cover agent reply path, expand-state hygiene, nested counts (MUL-1895)
Addresses three review issues from Emacs on PR #2300:
1. TaskService.createAgentComment bypasses Handler.CreateComment, so the
auto-unresolve wired into the handler did not fire when an agent replied
in a resolved thread (task / mention / on_comment paths). Extracted the
logic to TaskService.AutoUnresolveThreadOnReply so both reply paths share
it; rewired Handler.CreateComment to call the new method.
2. Resolving an already-expanded thread no longer collapses it back to the
bar because expandedResolved still contained the id. Added
clearResolvedExpand + handleResolveToggle wrapper so resolve / unresolve
always wipe the session expand entry.
3. ResolvedThreadBar received only direct children, while CommentCard's
expanded view recurses through descendants. Extracted the recursive
walk into thread-utils.collectThreadReplies and called from both —
counts and author lists now match.
Co-authored-by: multica-agent <github@multica.ai>
* test(comments): mock useResolveComment + add zh-Hans plural key
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
61ce8a8090 |
feat(daemon): add disk-usage CLI to surface per-task / per-workspace footprint (#2267)
* feat(daemon): add disk-usage CLI to surface per-task / per-workspace footprint Adds `multica daemon disk-usage [--by-workspace] [--by-task] [--top N] [--output json]`, walking the workspaces root to report task and workspace disk consumption without requiring a running daemon. Sizing reuses the GC artifact patternSet (basename-only) so the reported "artifact" footprint matches what `cleanTaskArtifacts` would actually reclaim, and the walk honors the same safety contract: never enters .git, never follows symlinks, counts only regular files. Refactors WorkspacesRoot resolution into an exported `ResolveWorkspacesRoot` so the read-only CLI picks the same root the running daemon would have. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): distinguish displayed totals from scan totals; add workspace artifact ratio - Track scan-wide TotalTaskCount / TotalWorkspaceCount on the report so `--top N` no longer leaves the table footer claiming the truncated row count is the full count. The CLI now prints a "Showing top N of M … Displayed: X. Scan total: Y" line whenever truncation happens, and keeps the bare "Total: …" footer for the un-truncated case. - Add ArtifactRatio (0..1) on WorkspaceDiskUsage and TotalArtifactRatio on the report. The workspace table renders an `ARTIFACT %` column. ratio() guards size=0 so empty workspaces report 0% instead of NaN%. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
823f124d67 |
feat(daemon): extend GC to chat / autopilot / quick-create tasks (#2260)
* feat(daemon): extend GC to chat / autopilot / quick-create tasks
Before this change the daemon's GC was strictly issue-centric: only tasks
with a non-empty issue_id ever wrote .gc_meta.json, and shouldCleanTaskDir
called only the issue gc-check endpoint. Chat / autopilot run / quick-create
tasks fell through to the GCOrphanTTL mtime path, which mis-killed active
chat sessions while leaving deleted ones around far longer than necessary.
Schema:
- GCMeta gains a Kind discriminator and per-kind ID fields
(ChatSessionID / AutopilotRunID / TaskID). WriteGCMeta now takes a
GCMeta struct so the call site classifies the task explicitly.
- ReadGCMeta defaults empty Kind to GCKindIssue, so legacy on-disk meta
files keep flowing through the issue path with no migration required.
Server endpoints (siblings of /api/daemon/issues/{id}/gc-check, all behind
requireDaemonWorkspaceAccess for the same anti-enumeration shape):
- GET /api/daemon/chat-sessions/{id}/gc-check -> {status, updated_at}
- GET /api/daemon/autopilot-runs/{id}/gc-check -> {status, completed_at}
- GET /api/daemon/tasks/{id}/gc-check -> {status, completed_at}
shouldCleanTaskDir dispatches on Kind:
- chat: active is hard-skipped (no mtime fallback) so idle sessions are
never reclaimed; archived + GCTTL cleans; 404 falls back to mtime to
stay safe for cross-workspace tokens.
- autopilot_run: terminal (completed/failed/skipped/issue_created) +
GCTTL cleans; running/pending skips. Uses run.completed_at as the TTL
anchor since autopilot_run has no updated_at column.
- quick_create: terminal task status cleans immediately (workdir is not
reused by the linked issue task, which has its own envRoot); running
skips.
Also drops the "skipping .gc_meta.json: issue_id is empty" warn — with
the new kind dispatch, chat/autopilot/quick-create tasks now write a
proper meta file instead of triggering this log.
Refs: GC follow-up to PR #2077 (symptom fix) and #2115 (chat hard delete).
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): chat gc-check 404 cleans immediately, no mtime gate
PR review caught that the chat 404 path was routing through
orphanByMTime, which deferred reclamation to GCOrphanTTL (72h) when
acceptance #3 calls for cleanup within one GC cycle (≤ 1h) after the
user hard-deletes a session.
Every chat_session_id we ever ask about was written by this same daemon
under its current token, so the cross-workspace probe defense the issue
path needs doesn't apply here. Drop the gate and clean on 404 directly.
Test updates:
- TestShouldCleanTaskDir_KindDispatch/chat_404 flips the locked
expectation from gcActionSkip to gcActionClean.
- Adds TestShouldCleanTaskDir_ChatHardDeletedFreshMtime: GCOrphanTTL
set to a year so any mtime-based path is unmistakably out, and the
fresh-mtime workdir still cleans on the chat-404 fast path.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
eb067ff077 |
fix(server): aggregate task_usage into daily rollup table to cut DB load (#2256)
* fix(server): aggregate task_usage into daily rollup table to cut DB load ListRuntimeUsage previously did a SUM(...) GROUP BY DATE(created_at), provider, model over the raw task_usage stream once per runtime row on the runtimes list and once per detail page load, scaling O(events) per call. This is the hot read path responsible for sustained load on Postgres. Switch the read path to a materialized daily rollup table maintained by a pg_cron job: - 072_task_usage_daily_rollup: schema for task_usage_daily + task_usage_rollup_state, plus rollup_task_usage_daily_window(p_from, p_to) (window primitive used by both cron and offline backfill, idempotent via ON CONFLICT DO UPDATE adding deltas) and rollup_task_usage_daily() (cron entry point — pg_try_advisory_lock(4242) for serialization, watermark advancement, 5-minute safety lag for late-visible inserts). Also adds idx_task_usage_created_at to help the two lazy endpoints (ListRuntimeUsageByAgent / GetRuntimeUsageByHour) that still hit the raw table. - 073_task_usage_daily_pgcron: CREATE EXTENSION IF NOT EXISTS pg_cron in a DO/EXCEPTION block (mirrors the migration 032 pg_bigm pattern so envs without shared_preload_libraries=pg_cron skip gracefully) and schedules rollup_task_usage_daily() every 5 minutes when the extension is present. - queries/runtime_usage.sql ListRuntimeUsage rewritten to read from task_usage_daily; sqlc regenerated. Other usage queries unchanged. - cmd/backfill_task_usage_daily: one-shot Go command that walks task_usage in monthly slices through rollup_task_usage_daily_window, then stamps the watermark to now()-5m so the cron resumes cleanly. Run once after migrations have applied, before relying on the rollup. - runtime_test.go: TestGetRuntimeUsage_BucketsByUsageTime now invokes rollup_task_usage_daily_window after fixture inserts so the handler sees the rolled-up rows. Synthetic daily rows cleaned up after each test. - runtime_rollup_test.go: new tests covering aggregation correctness, idempotency contract of ON CONFLICT DO UPDATE, and the watermark advancing exactly to now()-5m via the cron entry point. Deployment order: apply migrations → run backfill_task_usage_daily once → pg_cron picks up subsequent windows automatically. Today bucket may be up to ~10 minutes stale (5 min cron + 5 min lag) by design. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix(server): make task_usage_daily rollup safe to overlap, replay, and correct Addresses 4 review blockers on the original PR: 1. Cron/backfill double-count race: the rollup function is now idempotent. Window calls find DIRTY KEYS via task_usage.updated_at, then RECOMPUTE each bucket from ground truth and REPLACE the daily row (no more additive ON CONFLICT). Cron and backfill can now overlap safely. 2. Silent pg_cron absence: the read path is gated behind a new USAGE_DAILY_ROLLUP_ENABLED feature flag (default off). The raw task_usage scan is preserved as the fallback. Operators flip the flag per-environment after backfill + cron are confirmed healthy (task_usage_rollup_lag_seconds() helper added for monitoring). 3. UpsertTaskUsage corrections invisible to rollup: added task_usage.updated_at column (default now(), backfilled from created_at), and bumped it on conflict. Corrections now mark the bucket dirty and the next window call recomputes it correctly. 4. CREATE INDEX blocking writes on hot table: split into separate single-statement migrations using CREATE INDEX CONCURRENTLY (074, 075), matching the 035/067 pattern. Also: cron.schedule() removed from migrations entirely. Migration 076 only enables the extension (gracefully on unsupported envs); the actual schedule is a documented operator runbook step that runs AFTER backfill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix(server): trigger-driven invalidation + online-safe migration for task_usage_daily Round-2 review feedback on PR #2256: 1. Add explicit dirty-bucket queue (task_usage_daily_dirty) populated by triggers on agent_task_queue (UPDATE OF runtime_id, DELETE) and task_usage (DELETE). The rollup window function drains both this queue and the updated_at-based discovery, so runtime reassignment and issue-cascade deletes no longer leave the rollup divergent from the raw query. Triggers join via agent (not issue) to look up workspace_id, because when the cascade comes from issue, the issue row is already gone by the time atq's BEFORE DELETE fires; agent stays alive. 2. Make migration 072 online-safe: only ADD COLUMN updated_at TIMESTAMPTZ (nullable, no default → metadata-only ALTER, no row rewrite) and a separate ALTER for SET DEFAULT now() (also metadata-only). No bulk UPDATE on the hot task_usage table. The rollup window function's dirty_keys CTE handles legacy NULL rows via an OR branch, supported by partial index idx_task_usage_created_at_legacy. 3. Refresh stale documentation in cmd/backfill_task_usage_daily/main.go header to describe the current recompute/replace semantics, idempotent re-runnability, and the actual migration numbering (072..077). Tests: - TestRollupTaskUsageDaily_InvalidationOnReassign: verifies usage moves between runtime buckets after ReassignTasksToRuntime-style update. - TestRollupTaskUsageDaily_InvalidationOnIssueDelete: verifies daily bucket is cleared after issue delete cascades through atq → task_usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix(server): close dirty-queue race + move legacy partial index to its own concurrent migration Round-3 review feedback on PR #2256: 1. Blocker: dirty-queue invalidations could be silently lost under concurrency. ON CONFLICT DO NOTHING let a late trigger see the row already enqueued, no-op, and then the rollup drain (WHERE enqueued_at < p_to) would delete the original row — losing the late invalidation. Switched all three trigger enqueue paths to ON CONFLICT DO UPDATE SET enqueued_at = GREATEST(existing, EXCLUDED.enqueued_at), so any invalidation arriving during a rollup tick keeps enqueued_at > p_to (p_to = now() - 5min) and survives the post-tick drain. 2. High: idx_task_usage_created_at_legacy (partial index on hot task_usage table) was being created in the regular 077 migration without CONCURRENTLY. Moved to new migration 078 with CREATE INDEX CONCURRENTLY, matching the pattern of 074/075. 077's down migration leaves the index alone (it is owned by 078). 3. Minor: gofmt -w on runtime_rollup_test.go and backfill_task_usage_daily/main.go (tabs were lost in the original heredoc append). PR description rewritten to describe the current recompute/replace + dirty queue + feature flag design and the 072..078 migration ordering. Tests still green: TestRollupTaskUsageDaily_* (including both new invalidation regressions), TestGetRuntimeUsage_*, TestWorkspaceUsage_*. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix(server): unify workspace_id source via agent in rollup window function Round-4 review feedback (J) on PR #2256: M1 (must-fix): The dirty queue triggers resolved workspace_id via `agent.workspace_id`, but the window function's `dirty_from_updates` discovery and `recomputed` recompute join used `issue.workspace_id`. There is no schema-level FK guaranteeing `agent.workspace_id == issue.workspace_id`. Any divergence (future cross-workspace task scenarios, data repairs, migration bugs) would cause: - dirty queue rows with workspace_id from agent - recompute join filtering by workspace_id from issue - 0 matches in recompute → bucket erroneously hits the deleted_empty branch and the daily row is silently dropped - dirty_from_updates path attributing usage to the wrong workspace Replaced both CTEs to JOIN agent (not issue) so trigger / discovery / recompute share one workspace_id source. Comment in 077 explains the constraint. N1: Refreshed two stale references in cmd/backfill_task_usage_daily/main.go (header now says "072..078"; stampWatermark warning now mentions migration 073, where the rollup state table is actually introduced). Test: New TestRollupTaskUsageDaily_WorkspaceMismatch constructs an atq with agent.workspace_id != issue.workspace_id, asserts the bucket lands under agent's workspace (not issue's), and re-asserts after a runtime reassign in the foreign workspace. Acts as a canary if the schema invariant changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica.ai> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Devv <devv@Devvs-Mac-mini.local> |
||
|
|
9a3a99cef8 |
fix: make CLI short IDs routable
Make CLI table IDs routable across issue, autopilot, project, label, and task-run workflows. Adds scoped UUID-prefix resolution, --full-id table options, issue KEY display, safer actor/name output, and updated CLI docs/runtime prompt. |
||
|
|
d964d37f97 |
Revert "fix(cli): add --content-file / --description-file for non-ASCII on Wi…" (#2252)
This reverts commit
|
||
|
|
9650788709 |
fix(cli): add --content-file / --description-file for non-ASCII on Windows (#2247)
* fix(cli): add --content-file / --description-file for non-ASCII on Windows Windows PowerShell 5.1 (the Win11 default) and cmd.exe re-encode HEREDOC content through the active console codepage before piping it to a child process. Characters the codepage cannot represent are silently replaced with `?`, so agents on Chinese Win11 hosts emitting `--content-stdin` / `--description-stdin` HEREDOCs land all of their Chinese as `?` in the issue body and comments. The daemon log shows the original Chinese correctly because slog writes to a file directly, so the regression hides until the user opens the issue page. Add a `--content-file <path>` / `--description-file <path>` source to `resolveTextFlag`: the CLI reads the file straight off disk, preserves UTF-8 bytes verbatim, and skips the shell entirely. The runtime config injected into AGENTS.md / CLAUDE.md now surfaces this as the canonical Windows fallback when the daemon host runs on Windows; non-Windows hosts keep the existing stdin/HEREDOC guidance untouched. Closes #2198, #2236. Co-authored-by: multica-agent <github@multica.ai> * fix(execenv): route every Windows-host stdin directive at --content-file GPT-Boy on PR #2247 caught that the previous patch only inserted a Windows fallback into the Available Commands section. Two later prompt surfaces still hard-coded `--content-stdin` and overrode it for the agent: - The Codex-specific paragraph in `buildMetaSkillContent`, which always said "always use `--content-stdin` with a HEREDOC". - `BuildCommentReplyInstructions`, which is re-emitted on every turn for comment-triggered tasks (both via the AGENTS.md/CLAUDE.md workflow and the daemon's per-turn prompt) and mandated the same HEREDOC pipe. On Windows hosts we now branch both surfaces to a file-based template: the agent writes the body to a UTF-8 file with its file-write tool and posts via `--content-file <path>`. Non-Windows hosts keep the existing stdin/HEREDOC guidance untouched. Tests: - `TestBuildCommentReplyInstructionsWindowsUsesContentFile` pins the Windows / non-Windows reply-instruction text directly. - `TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin` asserts that the end-to-end CLAUDE.md / AGENTS.md surface for a comment-triggered Windows task has no remaining `--content-stdin` directive that could override the Windows fallback (covers Claude + Codex providers). Co-authored-by: multica-agent <github@multica.ai> * fix(execenv): make Windows comment block file-first, pin tests by GOOS GPT-Boy's second review on PR #2247 flagged two follow-up blockers: 1. The Windows comment/description block in `buildMetaSkillContent` was "stdin first, file caveat appended" — agents on Windows still saw "Agent-authored comments should always pipe content via stdin" / "MUST pipe via stdin" / `--description-stdin` directives before reaching the Windows fallback, so the contradicting instruction was live in the same prompt. Rewrite the entire Available Commands bullet for Windows hosts as file-first: the headline line names `--content-file`, the bulleted rules name `--content-file` / `--description-file`, and stdin only appears in anti-prescriptive "do NOT pipe via …" prose. 2. The existing non-Windows tests (TestBuildCommentReplyInstructions IncludesTriggerID, TestInjectRuntimeConfigDirectsMultiLineWritesToStdin, TestInjectRuntimeConfigCodexEmphasizesStdinForFormattedComments, TestInjectRuntimeConfigCommentTriggerUsesHelper) all depended on `runtimeGOOS` defaulting to non-Windows; they would silently fail on a Windows test runner. Pin them to `runtimeGOOS = "linux"` via save+restore and drop t.Parallel so they don't race with the GOOS-mutating Windows tests. Test additions: - TestInjectRuntimeConfigWindowsRecommendsContentFile now asserts the Windows AGENTS.md does NOT contain prescriptive stdin phrasings (`MUST pipe via stdin`, `use --description-stdin and pipe a HEREDOC`, `<<'COMMENT'`, `Agent-authored comments should always pipe content via stdin`, `always use --content-stdin`) on top of the file-first positive assertions. The ban list pins prescriptive substrings, not bare flag names, so anti-prescriptive prose like "do NOT pipe via --content-stdin" doesn't trip the ban. - TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin gets the same expanded ban list across the Available Commands, Codex paragraph, and per-turn reply template surfaces. - The non-Windows side of TestInjectRuntimeConfigWindowsRecommendsContentFile pins that the Linux stdin/HEREDOC contract is still in place, so a future refactor can't accidentally move every host to file-first. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cc527c34be |
perf(heartbeat): batch runtime last_seen_at writes (#2213)
Batches runtime heartbeat last_seen_at updates while preserving the 60s flush / 150s sweeper stale-window invariant. Also drains pending heartbeat writes during graceful shutdown. |
||
|
|
d82a2d8a04 | feat(skills): support importing skills from github.com URLs (#2209) | ||
|
|
5d51a0c9df |
feat(cli): add multica workspace update (#2191)
* feat(cli): add `multica workspace update` to edit workspace metadata Closes the CLI-side gap for #2178: the `PATCH /api/workspaces/{id}` endpoint and TS client method already exist, only the CLI subcommand was missing. Supports partial updates of name, description, context, and issue_prefix; long fields accept stdin via `--description-stdin` / `--context-stdin`. `slug` stays immutable, `settings`/`repos` are out of scope (deferred). Empty PATCH is rejected locally so we don't fire a no-op `EventWorkspaceUpdated` broadcast. Permission gate is unchanged (server-side admin/owner middleware). Co-authored-by: multica-agent <github@multica.ai> * fix(cli): address review on workspace update command - Reject `--issue-prefix ""` (and whitespace-only) explicitly. The server handler silently skips empty prefixes, so the previous behavior was a 200 OK with no actual change — exactly the kind of invisible no-op Emacs flagged in review. - Restore the `## Issues` H2 in the zh CLI reference. The earlier edit dropped it, leaving issue commands nested under the Workspaces section. Co-authored-by: multica-agent <github@multica.ai> * docs(cli): list `workspace update` in the en + zh top-level reference Mirrors the existing zh-only entry under apps/docs/content/docs/cli/ into the English overview so the new command is discoverable from both locales. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
38f777d0ba |
feat(autopilot): auto-pause autopilots with sustained high failure rate (#2136)
* feat(autopilot): auto-pause autopilots with sustained high failure rate Adds a background monitor that pauses any active autopilot whose recent runs are dominated by failures (defaults: ≥100 terminal runs in 7d, ≥90% failed). The monitor leaves a severity=attention inbox notification for the autopilot's creator (or the agent's owner if the autopilot was agent-created) so a human learns about the auto-pause and can fix the root cause before re-enabling. Motivated by MUL-1336 §6 #2: a single broken cron autopilot (`Registro de ls cada 5 min`, 1,475/1,476 failed in 7d) was burning ~1.5k tasks/tokens per week with no human in the loop. Tunable via AUTOPILOT_FAIL_MONITOR_{INTERVAL,LOOKBACK,MIN_RUNS,FAIL_RATIO,STARTUP_DELAY}; INTERVAL=0 disables the monitor entirely. Co-authored-by: multica-agent <github@multica.ai> * chore(autopilot): relax failure monitor defaults to daily / 50 runs Per review feedback in MUL-1339: 30-min scan was overkill — the 50-run threshold already provides multi-hour lag, and operational simplicity matters. Lowering MinRuns from 100 → 50 keeps low-frequency autopilots in scope (~7 runs/day reaches threshold within 7d window). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ba147708a6 |
fix(timeline): cursor-paginated timeline to stop long-issue freeze (#1968) (#2128)
* fix(timeline): cursor-paginated timeline to stop long-issue freeze (#1968) Opening an issue from Inbox with thousands of timeline entries used to hard-freeze the browser tab on a synchronous render of every comment + activity. The whole pipeline was unbounded: the API returned every row, TanStack Query cached the full array, and IssueDetail mounted N CommentCards (each running a full react-markdown + lowlight pipeline) in one frame. This swaps the timeline endpoint to keyset cursor pagination and rewires the frontend to useInfiniteQuery so a long issue costs the same as a short one on first paint. API: - GET /issues/:id/timeline now accepts ?before / ?after / ?around (mutex) + ?limit (default 50, max 100); response wraps entries with next/prev cursors and has_more flags. Cursors are opaque base64 (created_at, id). - ?around=<entry_id> anchors a window on the target so Inbox notifications pointing at an old comment never trigger the freeze. - New composite indexes on (issue_id, created_at DESC, id DESC) replace the redundant single-column ones so keyset queries are index-only scans. - /issues/:id/comments default branch now caps at 50 instead of returning every row unbounded; the unbounded ListComments / ListActivities sqlc queries are deleted. Frontend: - useIssueTimeline switches to useInfiniteQuery, exposes fetchOlder/fetchNewer/jumpToLatest + isAtLatest + newEntriesBelowCount. - WS handlers respect the at-latest invariant: comment/activity:created prepends to pages[0] only when the user is reading the live tail; otherwise it just bumps a counter so the UI offers a "Jump to latest" affordance without yanking scroll. - Optimistic mutations adapted to the InfiniteData shape via shared helpers (mapAllEntries / filterAllEntries / prependToLatestPage in core/issues/timeline-cache.ts) and use setQueriesData so all open windows of the same issue stay in sync. - IssueDetail Activity section gets a TimelineSkeleton placeholder during the brief load window plus subtle text-link load-more buttons matching the existing Subscribe affordance (no Button chrome). Top uses a divider for boundary clarity; bottom shows "Jump to latest · N new" weighted slightly heavier when there's unread state. - highlightCommentId now flows into the hook's around parameter so Inbox jumps fetch the surrounding 50 entries directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(agent): default comment list to 50 + prompt hint about long issues The CLI's "multica issue comment list" used to default to --limit 0 (meaning "fetch every comment"), which lets an agent on a long issue fill its context window with thousands of rows. The default is now 50; agents that need older history can pass --limit or --since explicitly. The local-coding-agent prompt also gains a single-line note about this in both the comment-triggered and on-assign flows so the agent knows to scope its fetches when issue size is unknown. Autopilot run-only mode is intentionally unchanged — it has no issue context to query. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
09f04847d3 | feat(server): redis-backed runtime liveness with DB fallback (#2121) | ||
|
|
a4fac51cf5 |
fix(projects): add resource_count breadcrumb instead of inlining resources (#2118)
* fix(projects): add resource_count breadcrumb to project responses Closes #2087 `multica project get` previously returned project metadata with no signal that resources existed. Agents that fetched a project this way had no way to discover its attached resources without already knowing about `/api/projects/{id}/resources` or the on-disk `.multica/project/resources.json`. Rather than inline the full resource list into the parent payload (which conflates parent metadata with a child sub-collection and locks the resource_ref shape into the project endpoint's contract), this adds a scalar `resource_count` breadcrumb to ProjectResponse. The actual list stays at the dedicated sub-collection endpoint. Changes: - GetProjectResourceCounts :many — new batched sqlc query - ProjectResponse.ResourceCount populated in GetProject, ListProjects, SearchProjects, and the with-resources CreateProject echo - multica project get prints a stderr hint pointing at multica project resource list <id> when count > 0; the JSON on stdout stays parseable - Meta-skill (runtime_config.go) lists multica project get and multica project resource list in Available Commands so agents that read CLAUDE.md / AGENTS.md know about both paths Co-authored-by: multica-agent <github@multica.ai> * fix(projects): wire ResourceCount through Update + Create event payload Review feedback on #2118. - UpdateProject now reloads ResourceCount before responding/publishing. Previously a title- or status-only PUT served (and broadcast over WS) resource_count: 0 even when resources existed. - The with-resources CreateProject path sets resp.ResourceCount before the project:created publish, so the WS event payload matches the HTTP echo. The hand-rolled response map collapses to an embedded ProjectResponse + resources array — one source of truth for the serialized shape. - packages/core/types/project.ts: Project gains resource_count: number to keep the TS contract aligned with the server response. Tests: - TestProjectResourceCountBreadcrumb extends to assert UpdateProject preserves the breadcrumb. - TestCreateProjectWithResourcesEchoesCount asserts the create echo carries resource_count matching the attached resources. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
60b215f44f |
feat(chat): support deleting chat sessions (#2115)
* feat(chat): support deleting chat sessions
Replaces the unreachable archive endpoint with a real hard delete and
exposes it from the chat history panel.
- DELETE /api/chat/sessions/{id} now hard-deletes the session and its
messages (CASCADE), cancels any in-flight tasks before removal so the
daemon doesn't keep running work whose result has nowhere to land,
and broadcasts chat:session_deleted.
- Frontend adds a per-row delete button with a confirmation dialog,
optimistically drops the session from both list caches, and clears the
active session pointer locally + on other tabs via the WS handler.
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): make session delete atomic and keep archived sessions read-only
Address review feedback on #2115.
- DeleteChatSession now runs lock + cancel + delete in a single tx and
only broadcasts events post-commit. The new LockChatSessionForDelete
query takes FOR UPDATE on chat_session, which blocks the FK validation
of any concurrent SendChatMessage trying to enqueue a task for this
session — that insert fails after we commit, so it can no longer
produce an orphaned task whose chat_session_id is nulled by
ON DELETE SET NULL. Cancel failure now aborts the delete instead of
warn-and-continue.
- SendChatMessage refuses non-active sessions again. The archive code
path is gone, but legacy rows with status='archived' may still exist
in the DB; keep the guard until we explicitly migrate them.
- Frontend re-reads allChatSessionsOptions to disable ChatInput on
legacy archived sessions so the UX matches the server-side guard.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
f1082b10a4 |
feat(cli): add --assignee-id / --to-id / --user-id for unambiguous targeting (#2114)
* feat(cli): add --assignee-id / --to-id / --user-id for unambiguous targeting
`multica issue {create,update,list}`, `issue assign`, and `issue subscriber
{add,remove}` accepted only fuzzy name matching, which fails in workspaces
where one user's name is a substring of another (e.g. agent "J" vs
"Cursor - J" / member "Jiayuan"). #1642 added UUID acceptance through the
existing flags, but there was still no explicit path that signals "this is a
UUID, not a name" — important for scripts that read IDs from
`multica workspace members --output json`.
Adds an `-id`-suffixed counterpart for every assignee-taking flag:
- `issue list` : --assignee-id
- `issue create` : --assignee-id
- `issue update` : --assignee-id
- `issue assign` : --to-id
- `issue subscriber {add,remove}` : --user-id
The new flags route through `resolveAssigneeByID`, a strict resolver that
requires a canonical UUID and fails with a clear error when the entity is
not in the workspace (no name fallback). A shared `pickAssigneeFromFlags`
helper enforces mutual exclusion between the name and id flags so a script
that accidentally sets both never silently applies one over the other.
Refs MUL-1254.
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): detect assignee flag presence via Changed, not value-emptiness
`pickAssigneeFromFlags` previously branched on `flag value != ""`, so
explicitly passing an empty UUID silently routed through the "no flag set"
path:
multica issue list --assignee-id "" # listed every issue
multica issue create --assignee-id "" # created an unassigned issue
multica issue subscriber add --user-id "" # subscribed the caller
This is exactly the failure mode the strict-UUID flag was added to prevent —
a script interpolating `--assignee-id "$MAYBE_UUID"` against a missing env
var should fail loudly, not silently degrade to a different operation.
Switch the picker (and the assign-command top-level guard) to use
`Flags().Changed`, so an explicit empty value reaches `resolveAssigneeByID`
/ `resolveAssignee` and surfaces a clear "expected a canonical UUID" /
"no member or agent found matching" error.
Co-authored-by: multica-agent <github@multica.ai>
* docs(cli): cover --assignee-id / --to-id in user docs and quick-create prompt
Follow-up to the --*-id flag rollout: surface the new flags everywhere the
old ones are documented so users (and agents) can discover them.
- assigning-issues.{mdx,zh.mdx}: the page explicitly calls out the
duplicate-name footgun ("first one listed wins, so rename before
assigning") — replace that workaround with a --to-id <uuid> example
- cloud-quickstart.{mdx,zh.mdx}: add a --to-id hint after the substring-
match callout so first-time users learn about the strict path
- internal/daemon/prompt.go (quick-create injected prompt):
- default-to-self: pass --assignee-id <task.Agent.ID> instead of
--assignee <name>; the picker agent's UUID is already in scope and
UUID matching is unambiguous in workspaces with overlapping agent
names (J / Cursor - J / Pi - J etc.)
- user-named: tell the agent to prefer --assignee-id <uuid> using the
user_id/id from the JSON it already fetched; --assignee <name> stays
a fallback for unambiguous workspaces
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
44a0ced558 |
fix(runtime): persist CLI update requests in Redis (#2113)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
862b0509df | feat: support repo checkout ref selection (#1988) | ||
|
|
ba5b7db78e |
fix(server): persist ModelListStore across replicas via Redis (#2022)
* fix(server): persist ModelListStore across replicas via Redis The model picker uses a pending-request pattern: the frontend POSTs to create a request, the daemon pops it on its next heartbeat, runs agent.ListModels locally, and reports back. Until now the store was a plain in-memory map per Handler instance. That works for self-hosted single-instance deploys but fails in any multi-replica environment (Multica Cloud). Each replica has its own map, so: POST /runtimes/:id/models → request stored in replica A GET /runtimes/:id/models/<requestId> → polls land on B/C → 404 daemon heartbeat → only A sees PendingModelList POST .../<requestId>/result → daemon's report has to land on A Success probability ~1/N². The visible symptom is "No models available" in the picker for every provider, even those (Claude/Codex) whose catalog is statically populated end-to-end. Same shape of bug, same Redis-backed fix as multica-ai/multica#1557 did for LocalSkillListStore / LocalSkillImportStore. Reuse the operational playbook (namespaced keys, ZSET-backed pending queue, atomic ZREM+SET-running via the shared Lua script) so we don't introduce a second concurrency model for the same primitive. Changes: - Convert ModelListStore from struct to interface with context-aware methods. Add HasPending for cheap heartbeat-side probing. - InMemoryModelListStore — single-node fallback, used when REDIS_URL is unset (self-hosted dev / tests). - RedisModelListStore — multi-node implementation using the same key layout and Lua atomic claim as RedisLocalSkillListStore. - Use RunStartedAt (not UpdatedAt) as the running-timeout reference point, matching the local-skill stores so subsequent UpdatedAt bumps don't reset the running clock. - Heartbeat now uses the probe-then-pop pattern for the model queue (matching local-skills) so a slow Redis can't stall every connected daemon. Extends heartbeatMetrics + slow-log with probe_model_ms / pop_model_ms / probe_model_timed_out for parity. - Wire the Redis backend in NewRouterWithOptions when rdb != nil. - Tests for both backends. Redis tests gate on REDIS_TEST_URL so laptop runs without Redis still pass; CI provides it. Co-authored-by: multica-agent <github@multica.ai> * fix(server): persist RunStartedAt + retry model report on transient failures Two follow-ups from PR #2022 review: 1. RedisModelListStore was dropping ModelListRequest.RunStartedAt on persistence — the field is tagged json:"-" so it doesn't leak into the HTTP response, which made plain json.Marshal(req) silently discard it. Across-node readers saw RunStartedAt=nil and applyModelListTimeout's running branch became a no-op, so the 60s running-timeout escape hatch never fired. CI's TestRedisModelListStore_RunningTimeout was failing on this exact case. Fix mirrors RedisLocalSkillImportStore's envelope pattern — wrap in an internal struct that re-promotes the field. HTTP shape stays clean. Adds a no-Redis unit test that pins the round trip. 2. Daemon's handleModelList called d.client.ReportModelListResult directly and swallowed any 5xx, leaving the pending request stranded in "running" until its 60s server-side timeout — exactly the failure mode the multi-node store fix was meant to eliminate. Generalize the existing local-skill retry helper into reportRuntimeResultWithRetry (kind: model_list / local_skill_list / local_skill_import) and wire handleModelList through a new reportModelListResult helper. Renames the test-overridable var localSkillReportBackoffs → runtimeReportBackoffs to match. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
972c65dbc1 |
fix(cli): make multica login --token accept the PAT as a value (#2017)
* fix(cli): make `multica login --token` accept the PAT as a value The flag was registered as a Bool, so `multica login --token <PAT>` parsed `--token` as `true` and dropped the supplied value as an unused positional argument, then unconditionally prompted "Enter your personal access token:". This contradicted the user-facing docs (`cli.mdx`, `CLI_AND_DAEMON.md`, the in-app `connect-remote-dialog`) which show `--token <mul_...>`. Switch `--token` to a String flag. Both `--token mul_...` and `--token=mul_...` now bind the value and skip the prompt. Passing `--token=` with an empty value (or `multica login --token=""`) still falls through to the interactive prompt for users who don't want the token in shell history. Updates the few internal docs that showed the no-value form. Fixes #1994 Co-authored-by: multica-agent <github@multica.ai> * fix(cli): preserve `multica login --token` (no value) prompt path and tighten regression test Addresses review feedback on #2017: 1. Restore the legacy no-value form. After the prior commit, `multica login --token` (no value) errored with `flag needs an argument: --token`, which broke the CLI_INSTALL.md / CLI_AND_DAEMON.md flow for headless users. Set `NoOptDefVal` on the `--token` flag to a sentinel that runAuthLoginToken treats as "prompt me," so: - `--token mul_xxx` and `--token=mul_xxx` consume the value (the #1994 fix is preserved), - `--token` alone falls through to the interactive prompt, - `--token=""` (explicit empty) also prompts. pflag with `NoOptDefVal` won't bind the next positional as the flag's value, so runAuthLogin recovers `--token mul_xxx` (the form from #1994) by promoting a single positional arg into the token. loginCmd gains `Args: cobra.MaximumNArgs(1)` so multi-positional typos still error fast. 2. Tighten regression coverage. Split into TestLoginTokenFlagWiring (asserts the production loginCmd.Flags().Lookup("token") is a String flag with the prompt-mode NoOptDefVal — would fail if anyone reverts the flag to Bool) and TestLoginTokenFlagParsing (drives all five documented invocation forms through the same flag wiring + the runAuthLogin space-form recovery). The synthetic-only test that the reviewer flagged is gone. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c2f199650a |
feat(cli): add agent avatar upload command (#1760)
* feat(cli): add UploadFileWithURL and AttachmentResponse to APIClient
* feat(cli): add agent avatar command and show avatar_url in agent get output
* fix(server): include id and url in no-workspace file upload response
* fix(cli): remove dead HTTPClient timeout swap, extend ctx to 60s for avatar upload
The 30s context deadline was tighter than the 60s HTTPClient timeout
swap, so the swap was dead code and did nothing for slow connections.
Both Neo and Omni Mentor flagged this in review.
Fix: extend the command context to 60s and remove the HTTPClient
mutation. This is simpler, thread-safe, and actually works for slow
uploads.
* fix: align fallback upload response shape and honor context deadline
- file.go: fallback returns {id, url, filename} instead of {filename, link},
matching the no-workspace path response shape.
- client.go UploadFileWithURL: tolerate empty attachment ID (S3 succeeded
but DB record failed — the file is still usable via its URL).
- client.go UploadFileWithURL: use a context-deadline-aware HTTP client so
that the 60s upload timeout set by the avatar command actually takes
effect instead of being shadowed by the default 15s client timeout.
- client_test.go: update 'missing id' test to verify empty-id success
(fallback tolerance).
* fix(cli): shallow-copy HTTP client to preserve Transport on upload timeout
When the context deadline exceeds the default 15s HTTP client timeout,
UploadFileWithURL was creating a bare &http.Client{Timeout: remaining},
silently dropping any custom Transport, Jar, or CheckRedirect configured
on the original client. This causes obscure connection failures when the
CLI uses an authenticated proxy, custom TLS, or mock transport in tests.
Fix: perform a shallow copy of the original client struct and only
mutate the Timeout field on the copy.
|
||
|
|
2dddfaa196 |
feat(daemon): Redis empty-claim fast path for /tasks/claim polling (#1860)
* feat(daemon): Redis empty-claim fast path for /tasks/claim polling Daemons poll /tasks/claim every 30s per runtime; the steady-state warm-empty case currently runs ListPendingTasksByRuntime against Postgres on every poll. This collapses that path: - New ListQueuedClaimCandidatesByRuntime query restricts to status = 'queued' (the old query also returned 'dispatched' rows that can never be reclaimed) and is backed by a partial index keyed on (runtime_id, priority DESC, created_at ASC). - New EmptyClaimCache caches the negative verdict in Redis with a 30s TTL. ClaimTaskForRuntime checks the cache before SELECT and populates it on confirmed-empty results. - notifyTaskAvailable now invalidates the runtime's empty key before kicking the daemon WS, so newly enqueued tasks become claimable immediately rather than waiting out the TTL. - AutopilotService.dispatchRunOnly now goes through TaskService.NotifyTaskEnqueued so run_only tasks get the same invalidate-then-wakeup contract as every other enqueue path. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): close MarkEmpty/Bump race in empty-claim fast path GPT-Boy's review on PR #1860 caught a real concurrency bug. Under the prior implementation it was possible for a slow claim to write an empty verdict AFTER a concurrent enqueue had already invalidated it: T1 claim: SELECT -> empty T2 enqueue: INSERT row, DEL empty key (no-op, key not set yet), wakeup T1 claim: SET empty (writes a stale "empty" verdict) T3 wakeup: IsEmpty -> hit -> returns null The just-queued task would then sit idle until the empty key's TTL expired (up to 30s). Replace the DEL-based invalidation with a per-runtime version counter: - CurrentVersion(rt) is a Redis INCR counter at mul:claim:runtime:version:<rt> with a 24h sliding TTL. - Claim samples version BEFORE the SELECT and passes it to MarkEmpty, which stores the verdict's value as the observed-version string. - IsEmpty MGETs both keys and trusts the verdict only when the empty-key value equals the current version. - Enqueue Bumps the version (INCR + EXPIRE) before the wakeup, causing any verdict written under a prior version to be rejected on the next read. Also bound every Redis call from this cache with a 250ms timeout — notifyTaskAvailable uses a background context so a wedged Redis must not block enqueue. Tests against a real Redis (REDIS_TEST_URL) cover: - MarkEmpty + IsEmpty under matching version returns hit - Bump invalidates a prior empty verdict (race-fix pin) - A MarkEmpty written under a stale pre-Bump version is rejected - TTL clamping, per-runtime isolation, nil-cache safety - notifyTaskAvailable Bumps before the wakeup fires Co-authored-by: multica-agent <github@multica.ai> * chore(daemon): renumber claim-candidate index migration to 067 Slot 064 was taken on main by 064_notification_preference. The migration runner tracks per-version in schema_migrations and would silently skip the second 064_*, leaving the index uncreated. Rename to 067 (next free slot). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b1345685a3 |
fix(task): rerun starts a fresh session, skip poisoned resume (#1928)
* fix(task): rerun starts a fresh session, skip poisoned resume
When a task ended in a known agent fallback ("I reached the iteration
limit and couldn't generate a summary.", "Put your final update inside
the content string. Keep it concise.") the (agent_id, issue_id) resume
lookup would still pick that session, so a manual rerun inherited the
poisoned state and reproduced the same bad output.
Two complementary guards:
1. Daemon classifies poisoned terminal output and routes it through the
blocked path with failure_reason set ('iteration_limit' /
'agent_fallback_message'). GetLastTaskSession excludes failed tasks
with those reasons, so even comment-triggered tasks no longer resume
them. Tasks that failed mid-flight (timeout, runtime_recovery, etc.)
are still resumable, preserving MUL-1128's auto-retry contract.
2. Manual rerun marks the new task force_fresh_session=true. The daemon
claim handler skips the resume lookup entirely when the flag is set,
capturing the user-intent signal that "the prior output was bad" even
when poisoned classification misses a future fallback wording.
Auto-retry of orphaned mid-flight failures (MaybeRetryFailedTask →
CreateRetryTask) does not take this path, so it keeps resuming.
Tests: classifyPoisonedOutput unit test; integration tests assert the
SQL filter excludes poisoned classifiers, RerunIssue flips the flag,
and the normal enqueue path leaves it false.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): cap poisoned-output matcher to short trimmed text
GPT-Boy review on MUL-1630: the previous strings.Contains match would
classify any output that quoted the marker substring — including a
review/analysis that simply discussed the marker itself. Real fallback
messages are short single-sentence affairs, so cap the candidate at
~one paragraph and trim whitespace before matching. Adds regression
tests covering a long quoting review and a marker buried in a long
real conclusion; both must stay classified as completed.
Co-authored-by: multica-agent <github@multica.ai>
* fix(migrations): rename 065 force_fresh_session → 066 to clear collision
main introduced 065_project_resources after this branch was cut, so
both files shared the 065_ prefix. The readiness check
(server/cmd/server/health.go → migrations.LatestVersion) takes the
last entry by lexical order, which is 065_project_resources, leaving
this branch's 065_force_fresh_session unguarded — a deploy that
applied project_resources but not force_fresh_session would still
report ready, and the next enqueue / rerun / claim would crash on
"column force_fresh_session does not exist".
Renaming to 066_force_fresh_session puts it strictly after
project_resources so readiness blocks until it's applied.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
44608713bb |
feat(projects): typed project resources + agent runtime injection (#1926)
* feat(projects): typed project resources + agent runtime injection
Adds a `project_resource` table that lets a project carry typed pointers
(github_repo today, more later) and surfaces them at agent runtime.
Server
- migration 065: project_resource (resource_type TEXT + resource_ref JSONB)
- sqlc CRUD + handler at /api/projects/{id}/resources
- claim handler attaches project_id/title + resources to issue tasks
Daemon
- TaskContextForEnv carries project context
- writes .multica/project/resources.json into workdir
- adds "## Project Context" block to CLAUDE.md / AGENTS.md / GEMINI.md
via type-dispatched formatter so new resource types just add a case
CLI
- multica project create --repo <url> attaches repos in one step
- multica project resource add/list/remove
Frontend
- Project create modal: Repos pill (workspace repos + ad-hoc URL)
- Project detail sidebar: collapsible Resources section with attach/remove
Docs
- New "Project Resources" chapter explaining the abstraction and
exactly what code to touch when adding a new resource type
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): transactional resources[] on create + generic CLI ref + test fix
Addresses review feedback on PR #1926:
1. CI red: TestProjectResourceLifecycle delete step called withURLParam
twice, which replaced the chi route context and dropped the project id.
Switched to the existing withURLParams helper from daemon_test.go.
2. POST /api/projects now accepts resources[] and attaches them in the
same transaction as the project. Invalid refs roll back the whole
create — no more half-attached projects on failure. Web modal + CLI
`project create --repo` both use the new bundled payload.
3. CLI `project resource add` now accepts a generic --ref '<json>' flag
so a new resource_type works without a CLI change. Per-type
shortcuts (--url for github_repo) remain as a convenience but are no
longer the only way in. Docs updated to drop the CLI from the
"files you must touch" list.
Adds two new server handler tests:
- TestCreateProjectAttachesResources (resources[] happy path)
- TestCreateProjectRollsBackOnInvalidResource (transactional rollback)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
72d5135bf0 |
fix(quick-create): subscribe requester to issues created via quick-create (#1924)
The agent runs the daemon CLI, so issue.creator_type is `agent` and the issue:created event listener only auto-subscribes the agent — not the human requester. Result: the requester gets a single completion inbox item but never sees follow-up comments or updates on their own issue. Subscribe the requester (reason=`creator`, the only matching value allowed by issue_subscriber's CHECK constraint without a migration) inside notifyQuickCreateCompleted, after the issue lookup succeeds and before the inbox write. Best-effort: log on failure, don't block the inbox. On success, publish subscriber:added so the UI stays in sync with manual subscribe and the listener-driven path. Adds two integration tests in cmd/server: success path subscribes the requester; failure path (agent finished without creating an issue) leaves no subscriber rows. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8c9c52b023 |
feat(inbox): add notification preferences to control inbox noise by event type (#1906)
Users can now mute specific notification categories (assignments, status changes, comments & mentions, priority/due-date updates, agent activity) from Settings > Notifications. Muted event types are silently filtered at notification creation time — no inbox items are created for muted groups. - Add notification_preference table (migration 064) - Add GET/PUT /api/notification-preferences endpoints - Filter notifications in notifyIssueSubscribers, notifyDirect, and notifyMentionedMembers based on user preferences - Add Notifications tab in Settings with per-group toggle switches |