mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 06:28:23 +02:00
v0.4.7
699 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9961db15ed |
feat(issues): bump issue updated_at when a comment is added (MUL-5009) (#5667)
* feat(issues): bump issue updated_at when a comment is added (MUL-5009) A new comment now counts as activity on its issue and advances updated_at, so the "Updated date" Kanban/list sort surfaces recently-discussed cards — not only cards whose status changed. Applies to all three comment-creation paths (user/agent HTTP, agent task delivery, and the child-done system comment) via a best-effort TouchIssue query. The bump never fails an already- persisted comment; it self-heals on the next activity if it errors. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): make comment updated_at bump atomic (MUL-5009 review) Address Elon's review. Move the updated_at bump into CreateComment as a leading data-modifying CTE so the comment insert and the timestamp bump commit or roll back together — closing the non-atomic window where a comment could persist while updated_at stayed stale. That window also skewed the daemon GC TTL, which reads issue.updated_at to reclaim done/cancelled workdirs. Centralizing the bump in the query drops the three per-caller TouchIssue calls and guarantees any future comment entrypoint inherits it. Also refresh the now-stale gc.go / gc_test.go comments that asserted 'CreateComment does not bump issue.updated_at'. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): make comment/issue workspace match a query-level guarantee (MUL-5009 nit2) The touch CTE now RETURNING id, workspace_id and the INSERT SELECTs from it, so the comment insert depends on the issue actually existing in the passed workspace. A mismatched (issue, workspace) pair matches 0 rows in the CTE, the dependent INSERT selects nothing, and the :one query returns pgx.ErrNoRows — no mis-attributed comment is written and the issue is not touched. CreateComment is now the single carrier of the 'a comment belongs to an issue in the same workspace and always bumps it' invariant, so no future caller can break it by passing the wrong workspace. Signature unchanged; no migration or foreign key. Add TestCreateComment_WorkspaceMismatchPersistsNothing (error returned, no comment persisted, updated_at unchanged). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ed57707bb2 |
MUL-4923: bound daemon task preparation time (#5584)
* fix(daemon): bound pre-start task preparation Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): isolate pre-start env preparation Run execution-environment Prepare and Reuse in a killable helper process so a timed-out attempt cannot keep writing after retry. Add FIFO lifecycle and squad Stage retry regression coverage. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): terminate Windows prepare process trees Assign the pre-start helper to a kill-on-close Job Object before releasing its request, wait for all job members to exit on cancellation, and add a Windows runtime regression job. Co-authored-by: multica-agent <github@multica.ai> * ci: target Windows prepare tree regression Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1483ce0825 |
fix(agents): always enable AI creation (MUL-4998) (#5660)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
002ea0d879 |
MUL-4797: add configurable issue table view (#5454)
* feat(issues): add configurable table view Co-authored-by: multica-agent <github@multica.ai> * test(issues): cover table columns in page fixture Co-authored-by: multica-agent <github@multica.ai> * fix(issues): make table column picker interactive Co-authored-by: multica-agent <github@multica.ai> * fix(issues): repair quick create and virtualize table rows Co-authored-by: multica-agent <github@multica.ai> * fix(issues): keep pinned table cells opaque Co-authored-by: multica-agent <github@multica.ai> * fix(issues): anchor full-width table rows Co-authored-by: multica-agent <github@multica.ai> * fix(issues): consolidate table controls Co-authored-by: multica-agent <github@multica.ai> * fix(issues): harden table pagination and export Co-authored-by: multica-agent <github@multica.ai> * feat(issues): add table quick search Co-authored-by: multica-agent <github@multica.ai> * fix(issues): make table window filters, selection, and export authoritative Round-2 review fixes for the issues table (MUL-4797): - Send the agents-working filter as a server ids facet so matches on unfetched pages surface and total/pagination/export agree; a present- but-empty id list yields an empty window instead of an unfiltered one. - Reset surface selection when the membership window changes and act on selection ∩ visible rows in the batch toolbar, so batch actions, Export selected, and the count all share one authoritative set. - Materialize the full flat window while table grouping is active, and suspend hierarchy nesting / parent-based grouping until the window is complete so structure cannot reshuffle as pages arrive; suppress header facet-count badges while the table window is partial. - Resolve actor directories and the property catalog at export time and fail the export instead of writing Unknown* actors or dropping configured property columns on cold/errored lookups. - Append a unique id tie-break to the list/grouped ORDER BY and mirror it in compareIssuesForSort so offset pages are stable across same-timestamp ties. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): bound table structure window and align chip/transport/selection Round-3 review fixes for the issues table (MUL-4797): - Cap whole-window materialization at TABLE_STRUCTURE_MAX_WINDOW (1000): below it the remaining pages load automatically — hierarchy applies without scrolling to the last page — and above it grouping/hierarchy suspend with an explicit toolbar notice instead of triggering an unbounded workspace download from a persisted view option. - Give the agents-working chip the authoritative in-window running set (the ids-facet window query, shared key with the filter-on state) so its badge can no longer say 0 while the filter would find matches on unfetched pages; falls back to loaded-row scoping elsewhere. - Route ids-facet windows through a new POST /api/issues/query twin — hundreds of running-issue UUIDs overflow the ~8 KB GET request-line budget of common proxies. The body carries the same key/value pairs; the handler rebuilds the query string and delegates to ListIssues. - Reset surface selection during render (key-change pattern) instead of a post-commit effect, so no frame ever pairs new membership with the old selection. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): harden table auto-pagination against errors and stale totals Round-4 review fixes for the issues table (MUL-4797): - Stop the structure materialization loop (and the scroll sentinel) when the window query is in error state — a persistently failing page left hasNextPage true and isFetchingNextPage false after every attempt, so the ungated effect refired forever. Resuming is an explicit toolbar Retry. The advancement decision now lives in a pure, tested shouldAutoLoadNextStructurePage helper. - Make the structure ceiling a hard stop: the ceiling check reads the LATEST page's total (pagination already advances on it, so a stale small page-1 total could re-open unbounded materialization), and the loop additionally halts on loaded count >= ceiling regardless of any reported total. - Drive the working (ids-facet) window to completion — it is inherently bounded by the running set — and treat it as the chip's authoritative scope only when complete, so >100 running issues no longer under-count as a single page. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): make working-window pagination capped and unknown-aware Round-5 (final) review fixes for the issues table (MUL-4797): - The working (ids-facet) window now advances through the same shouldAutoLoadNextWindowPage gates as the structure loop — it shares the main table's cache key while the agents-working filter is on, so an uncapped chip-driven loop re-opened the very ceiling the table just enforced. An over-ceiling window stops after page one. - A cold-load failure of the flat window is an ERROR state, not an empty workspace: isEmpty only claims empty on a successful zero-result fetch, and the surface renders a dedicated failed-to-load state with a reachable Retry (the in-table Retry never mounted without data). - The chip scope is now tri-state honest: a COMPLETE window (or an empty running set) yields a precise count, keepPreviousData carries the last-known-complete set across re-keys, and everything else — cold resolving, failed, over the ceiling — presents as an explicit unknown ('Agents working: —') instead of a number derived from whichever incomplete window happened to be loaded. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): single pagination owner and placeholder-honest chip scope Round-6 review fixes for the issues table (MUL-4797): - Exclude placeholder data from the working-window completeness gate: on a re-key (running set or facet change) keepPreviousData leaves the OLD key's rows visible, and pairing them with the new task snapshot published a precise-looking number for a scope nobody fetched. The scope now reads unknown until the new key resolves. - Make the shared table query single-owner while the agents-working filter is on: the chip's background loop no longer answers the same render snapshot as TableView's structure loop, and every auto caller (structure loop, working loop, scroll sentinel, retry) now uses fetchNextPage({cancelRefetch: false}) so a concurrent responder no-ops instead of cancel/restarting a fetch whose HTTP request is not abortable — which had been duplicating every offset. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
465546b83b |
feat(autopilots): redesign the autopilot schedule editor (#5457)
Replace the free-form trigger-config form with a structured schedule editor built on an orthogonal cron model: separate frequency, time, and day-of-week/day-of-month dimensions map to and from cron expressions via a dedicated grammar and mapping layer, with validation and a human-readable describe() summary. The grammar suite drives the editor against a combinatorially generated corpus of 51,755 distinct cron expressions - every token form of every field, crossed - each judged against a reference robfig/cron v3 parser. Add a server-side /autopilot/cron-preview endpoint (plus schema and React Query hook) so the editor shows upcoming run times, and echo wildcard-carrying cron lists correctly instead of collapsing them. Supporting pieces: timezone-aware formatting helper, segmented-toggle and debounced-value utilities, a reworked time-input, and refreshed en/ja/ko/zh-Hans locale strings. |
||
|
|
1507997272 |
fix(agent): stop agents shipping local-path links, make Desktop 404 recoverable (MUL-4899) (#5557)
Agents were writing runtime-local paths into deliverables as clickable links (`[screenshot](/Users/agent/work/shot.png)`). Two root causes, both fixed here. A. The brief never stated the delivery contract. Add an always-on delivery invariant (outside writeOutput's kind switch, so no task kind can inherit none) plus a per-surface file-delivery line for each of the five surfaces. Chat splits into two: `attachment upload` works only on web/mobile chat, never on an IM channel, so ChatChannelType is now threaded into TaskContextForEnv. The claim path only ever looked up Slack bindings, so a Feishu session reported as a web chat and got upload guidance for a channel that cannot carry attachments. Probe every channel type. The chat policy is two independent layers and stays that way: delivery keys off "is there a channel at all"; the `chat history` / `chat thread` commands stay Slack-only because both endpoints are hardwired to h.SlackHistory and there is no Feishu reader — ChatInThread only selects between those two commands, so it stays Slack-only too. Add a CLI hard-fail lint on `issue comment add` / `issue create` / `issue update` as the enforcement backstop. Scoped narrowly, since a false positive blocks a real deliverable: agent task context only (a human's PAT run is untouched), real CommonMark link/image/autolink destinations only via goldmark (a path in a code span or fence — how an agent quotes a path it is discussing — is structurally invisible), and three high-confidence signals only (`file://`, inside the workdir, or an existing local file). A bare `/foo` is a valid origin-relative URI and is deliberately allowed. `issue update` has no --attachment flag, so its hint redirects to `comment add` rather than naming an argument it rejects. B. Desktop presented the resulting router 404 as an unknown crash. 8 of 18 desktop_route_error reports were users clicking such a link and being told the app broke and to file a bug. Split the 404 into a first-class Not Found view: no crash framing, no Report error. Its recovery entry comes from the tab store's active workspace, never from the failed pathname — deriving a slug from `/Users/me/shot.png` yields "Users" and a button to `/Users/issues`, a second 404. Also add a will-navigate trusted-origin guard via the shared loadRenderer (main + issue windows). This is origin hardening only, NOT the mechanism for in-app links: client-side routing never fires will-navigate, so app paths never reach it. Issue windows need no 404 work — their router only accepts paths validated by parseIssueWindowPath and they do not listen for multica:navigate, so a bad path cannot reach them. Server-side completion observation is metric/log only and never blocks: it is lexical (`file://` + task work_dir prefix) because the server cannot stat the daemon's filesystem, and the metric label is a closed enum so no path or reply text reaches Prometheus. Verified: pnpm typecheck/lint/test (3582 tests), go vet, full Go suite including new claim-path integration tests. cmd/multica was verified outside the daemon workdir — inside one, 93 of its tests fail identically on origin/main because the suite walks up and finds the runtime's own task marker. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
18d41151eb |
feat(gc): batch issue reconciliation (#5534)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ed9adc2bbe |
feat: improve create issue field controls (#5532)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ae68799c64 |
feat(task): reuse workdir on manual retry, gate session on error class (MUL-4869) (#5525)
* feat(task): reuse workdir on manual retry, gate session on error class (MUL-4869)
A manual retry (execution-log "retry" button -> POST /api/issues/{id}/rerun)
previously set force_fresh_session=true unconditionally, so the daemon threw
away the failed run's workdir and started from an empty one. For a transient
failure (network blip, provider 5xx / rate-limit, runtime_offline, timeout)
that discards work the agent already did.
New contract: a retry ALWAYS reuses the source task's workdir when it still
exists on disk; only the agent SESSION is gated, on whether the source task's
failure poisoned the conversation.
- RerunIssue derives force_fresh_session from the source task's failure_reason
via resumeUnsafeFailureReason instead of hardcoding true. Transient/cancelled
failures resume the session; conversation-poisoning failures start fresh.
- The daemon claim handler resolves the retry's session/workdir precisely from
rerun_of_task_id, not the most-recent (agent,issue) row that a parallel task
could hijack. PriorWorkDir is returned whenever present; PriorSessionID only
when resume-safe AND on the same runtime.
- force_fresh_session now gates only the session, never the workdir.
- Add agent_error.context_overflow to the resume-unsafe set (Go helper plus the
GetLastTaskSession / GetLastChatTaskSession blacklists): resuming an overflowed
context would immediately overflow again.
Objectively-unreusable cases (workdir GC'd, different runtime, failed before a
workdir was recorded) fall back to a fresh Prepare via the daemon's existing
execenv.Reuse / gateResumeToReusedWorkdir path, so no daemon change is needed.
Tests: RerunIssue force_fresh classification by failure_reason; claim-layer
workdir-always-reused plus session gating (same/different runtime, poisoned);
context_overflow classifier.
Co-authored-by: multica-agent <github@multica.ai>
* fix(task): make manual-retry workdir reuse rollback-safe and error-text aware (MUL-4869)
Addresses code review on #5525.
- Rollback safety: RerunIssue no longer writes force_fresh_session based on the
source failure. Rerun rows are pinned to force_fresh_session=true again, so an
OLD claim handler picked up mid rolling-deploy degrades to a clean start
instead of falling back to the (agent, issue) most-recent lookup and resuming
a *different* execution. The new claim handler ignores the flag for reruns and
computes session reuse from the exact source task (rerun_of_task_id).
- Legacy poison defense: add shared service.ResumeUnsafeFailure(failureReason,
errorText), which combines the failure_reason poison set with the same
400/invalid_request_error raw-error-text guard GetLastTaskSession applies. The
claim handler's exact-source path uses it, so legacy 'agent_error' /
deploy-window rows carrying a 400 marker are no longer resumed.
- CI: TestRerunIssueTargetsSourceTaskAgent passes again (rerun rows stay
force_fresh_session=true). Service test now asserts that rollback-safe
invariant across failure classes; the claim test drives session gating from
the source task and adds a legacy-400 case.
- Docs: tasks.{mdx,zh,ja,ko}.mdx and the GetLastTaskSession SQL comment now
distinguish execution-log per-row retry (task_id: reuse workdir, conditional
session) from CLI/API rerun (no task_id: fresh session + fresh workdir).
- Clarify cross-runtime workdir is best-effort: the daemon offers the source
workdir regardless of runtime (a shared mount may resolve it) and only the
per-cwd session is runtime-gated.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
69744241e8 |
fix(comment): restore autopilot @mention delegation authority (MUL-4857) (#5512)
* fix(comment): restore autopilot @mention delegation authority (MUL-4857) A schedule/webhook autopilot run is unattributed by design (no top-of-chain human originator, MUL-4302). Since MUL-3963 the A2A invoke gate (canInvokeAgent) keys on that originator, so a mid-run @agent/@squad delegation on an autopilot-created issue fails closed for the DEFAULT private agent (and member-scoped public_to agents): the mention renders but no run is enqueued. The SAME autopilot's first dispatch is admitted via the autopilot creator (autopilotAdmitInvoke -> canCreatorInvokeAgent), so first-dispatch and mid-run delegation disagreed. Align them: when an unattributed agent/system-authored comment on an autopilot-origin issue reaches computeCommentAgentTriggers with no originator, fall back to the autopilot creator as the effective invoking user for the gate. The gate still runs (no unrestricted agent-to-agent bypass); it is authorization only -- the enqueued task's originator/attribution stays unattributed. Scoped to autopilot-origin issues so other unattributed chains stay fail-closed. Adds a DB-backed regression test covering: creator-owns-target admits, a non-autopilot unattributed run stays denied, and a creator without invoke rights stays denied. Co-authored-by: multica-agent <github@multica.ai> * fix(comment): bind autopilot @mention authority to verified task lineage (MUL-4857) Address the review's confused-deputy finding on the P0 fix. The first cut keyed the invoke-gate fallback on issue provenance + an empty originator alone (invokeAuthorityForAutopilotIssue took only the issue), so any unattributed run could borrow a stranger autopilot creator's rights merely by commenting on that autopilot's issue — and the fallback also leaked past explicit @mention into the plain-comment squad-leader path and system actors. Rework it so the autopilot-creator authority is granted ONLY when the SPEAKING task's lineage is verified against this issue: - resolve the authority separately (new AutopilotDelegationAuthorityUserID on commentTriggerComputeOptions), never by overwriting OriginatorUserID; the gate reads it through opts.effectiveInvoker() only when no human originator resolved, so attribution stays untouched; - resolve from a server-trusted speaking task — X-Task-ID on create/preview, comment.source_task_id on edit/reconcile — via autopilotDelegationAuthority, which admits only when author == task agent AND task.issue_id == this issue AND the issue is autopilot-origin, then keys on the member autopilot creator; - do NOT key on autopilot_run_id: in create_issue mode (the reported case) the leader task is enqueued through the ordinary issue-assignment path and has no autopilot_run_id — the task.issue_id == issue binding is what proves the run is part of this autopilot's work while rejecting foreign-issue runs. Tests: replace the provenance-only regression with lineage-bound coverage — verified-lineage-admits, creator-without-rights-denied, non-autopilot-denied, missing-source-task-denied, cross-issue-source-task-denied, author!=task-agent- denied — plus an end-to-end CreateComment path asserting the private worker is enqueued and the delegated run stays unattributed. Verified the fallback is load-bearing (positive + e2e fail with it disabled) and the full internal/handler package passes. Skill docs (multica-mentioning) updated to the lineage-bound contract and new helper names. Co-authored-by: multica-agent <github@multica.ai> * fix(comment): make autopilot @mention authority consistent across defer/edit (MUL-4857) Second review round (Elon) surfaced two must-fixes on top of the lineage binding. 1. Busy-target completion reconcile lost the authority. A delegation to a target that is already running is deferred to that target's completion reconcile (reconcileCommentsOnCompletion). That path recomputed triggers with only the (empty) originator, so an unattributed autopilot delegation's follow-up was gate-denied again and silently dropped. It now restores the delegation authority from comment.source_task_id, so the follow-up fires once the target frees up — still unattributed. 2. Edit could borrow the old authoring run's authority, and preview != save. The edit preview keyed authority on the current request task while save keyed it on the comment's original source_task_id, so an agent editing its old autopilot comment from a task on an UNRELATED issue would fail-closed in preview but reuse the old autopilot creator's authority on save (cross-issue confused-deputy, and a preview/side-effect divergence). Fix: treat source_task_id as the persisted per-action authority lineage and re-stamp it on edit to the CURRENT editing task, issue-scoped exactly like CreateComment. A cross-issue edit re-stamps it to NULL, so preview, save, AND the deferred reconcile all fail closed identically. UpdateComment query gains a source_task_id param (sqlc regen). Also locks the review-accepted behavior that effectiveInvoker() carries the autopilot-creator authority into the plain assigned-squad-leader wake (a worker's result comment on the autopilot issue can still wake the private leader). Tests: reconcile-restores-authority (owns -> one unattributed follow-up; no rights -> none); edit re-stamp (same-issue keeps authority and triggers; cross-issue clears source_task_id and fails closed); worker-result wakes private squad leader. Verified both fixes are load-bearing (each negative control reproduces the exact regression Elon described), full internal/handler + internal/service packages pass, gofmt/vet clean. Skill docs (multica-mentioning) updated. Co-authored-by: multica-agent <github@multica.ai> * fix(comment): clear stale task lineage on non-author comment edits (MUL-4857) An admin editing an autopilot Agent's comment previously preserved the comment's original source_task_id. The immediate save is judged on the admin's member identity and correctly fails closed, but the deferred completion-reconcile routes the comment under its original agent author and resolved the delegation authority from the stale source_task_id, resurrecting the autopilot creator's invoke authority once the busy target freed up — an admin (manage rights) could thereby trigger another owner's private agent (invoke rights). Now a content edit re-derives lineage from the edit action: only the agent author editing its own comment re-stamps source_task_id to the current editing task; every other editor (member/admin, or any non-author) clears it, so preview, save, and reconcile all fail closed. Adds a regression covering the admin-edit + busy-target path and syncs the multica-mentioning skill docs. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6b2097ccbb |
feat(inbox): archived notifications sub-view (MUL-3736) (#5518)
Adds an "Archived" sub-view to the Inbox, reachable from an entry at the
bottom of the main list, with per-row unarchive. Mirrors chat's archived
sub-view so the two surfaces share one mental model.
Backend:
- GET /api/inbox/archived and POST /api/inbox/{id}/unarchive. Kept off the
existing GET /api/inbox so installed clients keep their contract and the
unbounded archive never rides along with the main list.
- The archived query excludes any issue that still has an active row. Archiving
is issue-level, so a new notification on an archived issue leaves old archived
rows beside a fresh active one — without the guard the issue renders in BOTH
lists. The exclusion lives in SQL so neither list depends on the other's cache.
- Unarchive is issue-level (mirroring archive) and leaves `read` untouched, so a
restored unread item raises the unread badge again.
- v1 ships no pagination: LIMIT 200, newest-first, so truncation drops the
oldest rows and never hides a group's newest one.
- inbox:unarchived event, fanned out to the recipient like the other personal
inbox events.
- Two CONCURRENTLY-built indexes; inbox_item previously had none covering
workspace/archived/created_at.
Frontend:
- Separate TanStack cache per list; every inbox event invalidates the workspace
prefix, since any of them can move an item across the boundary.
- View persisted as ?view=archived, so refresh, back/forward, and the mobile
detail-back all return to the list the user was in.
- Batch actions stay main-view only — they archive from the MAIN inbox, so
offering them over the archived list would do the opposite of what it reads.
- Mobile subscribes to inbox:unarchived (its list gains the restored row); its
own archived view remains follow-up.
Known debt: no pagination, so an archive past ~200 rows is truncated silently
in the UI; the entry's count is the deduplicated count of the rows returned.
Verified: pnpm typecheck/test/lint (0 errors), go build/vet, Go inbox suite
against a real Postgres, migrations up+down, and EXPLAIN confirming both new
indexes serve the query.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
2d9cf045d0 |
MUL-4861: align runtime CLI update permissions (#5514)
* fix(runtimes): align CLI update permissions Co-authored-by: multica-agent <github@multica.ai> * fix(runtimes): address update review nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4fcb27a729 |
fix(issues): attach labels in the create transaction (MUL-4832) (#5510)
* fix(issues): attach labels in the create transaction (MUL-4832) Labels chosen at issue creation were attached in a second, non-atomic round-trip after the issue was already committed (web modal looped attachLabelToIssue per label; the create endpoint took no labels). A partial failure of that follow-up left the issue committed but mis-categorized, surfaced only as a toast. Carry label_ids through the create request instead: the service validates them (workspace + resource_type='issue') and attaches them inside the same transaction as the issue insert, so the issue and its labels commit together or not at all. An unknown or wrong-scope label id now fails the whole create with 400 rather than being silently dropped. Duplicate ids are idempotent (dedupe + ON CONFLICT DO NOTHING). - server: IssueCreateParams.LabelIDs + ErrIssueLabelNotFound; validate and attach in IssueService.Create; handler parses label_ids and maps the error to 400. - web: create-issue modal forwards label_ids and drops the post-create attach loop and its dead toast key. - tests: handler coverage for atomic attach, stale-id 400 (no issue left behind), duplicate-id idempotency, wrong-scope rejection; web test asserts label_ids is forwarded. Scope is deliberately labels-only. The contributor PR #5475 also auto-parsed an acceptance_criteria field from description text; that introduces a new user-facing data contract with no defined edit/display rules and is left out for a separate product decision. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): echo created labels + guard old-backend compat (MUL-4832) Addresses review of #5510. 1. Create response + issue:created event now carry the labels attached in the create transaction. IssueService.Create returns the authoritative label snapshot; the handler sets it on both the HTTP response and the issue:created broadcast payload. Without this, online members other than the creator saw the new issue unlabeled indefinitely (staleTime: Infinity, no invalidation) because this PR removed the old post-create issue_labels:changed broadcast. 2. New web + old backend compatibility. During the rolling deploy window the web app can run ahead of the backend (web auto-deploys on merge, backend deploys manually). An older backend ignores label_ids and returns an issue with no labels field. The create modal now falls back to the legacy per-label attach only when the response omits labels, so labels aren't silently dropped; when labels is present the atomic path already ran and no fallback fires. The backend always returns an explicit labels array (empty when none) as the detection signal. Tests: handler issue:created-carries-labels + response-carries-labels + response-always-includes-labels; web modal no-fallback (new backend) and fallback (old backend); ws-updaters keeps the label snapshot in list cache. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): validate create response through a schema (MUL-4832) Addresses review of #5510 (must-fix 3). The create modal keys its label-attach compatibility fallback off `issue.labels` being absent (older backend) vs a validated Label[] (current backend). But api.createIssue cast the raw JSON straight to Issue, so a malformed labels value (null, an object, a garbage array) would be !== undefined and wrongly suppress the fallback, caching a bad shape — violating the repo's API Compatibility rule. Parse the create response through CreateIssueResponseSchema: - labels absent -> undefined (older-backend signal; fallback runs) - labels valid -> Label[] (fully validated elements, not z.unknown()) - labels malformed -> undefined via .catch (safe: never masquerades as handled; worst case a redundant re-attach, never a silent drop) A whole-body parse failure degrades to EMPTY_ISSUE (never throws into React), matching the existing parseWithFallback contract. Tests: schema.test.ts covers absent / valid / null / wrong-element-shape labels and the empty-issue degrade. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): reject a malformed create response instead of faking success (MUL-4832) Follow-up to review of #5510. The prior fix degraded a schema-failed create response to EMPTY_ISSUE, which React Query treats as a successful mutation: the empty issue is written into the list cache, a blank "created" toast shows, the "View issue" link points at an empty id, and with labels the fallback attach runs against an empty issue id. A create that returns an unusable body is a failed mutation, not a safe-empty read. Fall back to null and reject: mutateAsync is already inside the create modal's try/catch, so a controlled rejection preserves the draft and shows the failure toast, and onSettled still refreshes the list so a genuinely-created issue can still surface. - CreateIssueResponseSchema tightens id to non-empty; an id-less body routes to the same reject path. - createIssue throws on parse failure (empty-message Error so the modal renders its localized "failed to create" toast; parseWithFallback already logged the schema issues + raw body). - Dropped the EMPTY_ISSUE fallback constant. - Tests: whole-body malformed and empty-id now assert createIssue rejects; only-labels-malformed still returns the real issue with labels undefined. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
59ca4641f7 |
SNDBX-491 refresh autopilot project binding at dispatch (#5499)
Co-authored-by: Asere AI <asere.ai@icloud.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ea8511340e |
MUL-4820: support custom property icons (#5468)
* feat(properties): add custom icons Co-authored-by: multica-agent <github@multica.ai> * fix(migrations): use unique property icon prefix Co-authored-by: multica-agent <github@multica.ai> * fix(properties): replace emoji icons with Lucide picker Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
19e52e007c |
MUL-4798: make Inbox notification preference updates atomic (#5451)
* fix(notifications): make preference updates atomic Co-authored-by: multica-agent <github@multica.ai> * fix(notifications): serialize preference mutations Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f46d5d7ba5 |
fix(db): make issue property migrations deploy-safe (#5456)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f2c4b2cf50 |
MUL-4775: fix(daemon): negotiate websocket RPC support
* fix(daemon): negotiate websocket RPC support * fix(daemon): bind RPC negotiation to WS connection Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): gate claims without constraining WS transport Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b85bb71a58 |
feat: custom issue properties — typed workspace-defined fields with list-surface support (MUL-4463) (#5335)
* feat(server): custom issue properties — definitions, typed values, CLI (MUL-4463)
Workspace-level property definitions (issue_property table; 7 types:
text/number/select/multi_select/date/checkbox/url) plus a typed value bag
on each issue (issue.properties JSONB keyed by definition UUID, mirroring
the metadata machinery: single-key atomic writes, 16KB cap, GIN index).
- Definitions: owner/admin only; agent actors rejected (agents propose,
humans confirm). 20 active per workspace, 50 options per select,
reserved built-in names blocked, archive instead of delete.
- Values: any member or agent; per-type validation with self-correcting
error messages that enumerate legal option ids.
- API: /api/properties CRUD + PUT/DELETE /api/issues/{id}/properties/{propertyId};
issue responses always emit the properties bag.
- CLI: multica property list/get/create/update/archive/unarchive and
multica issue property list/set/unset with name→id translation.
- Events: property:created/updated, issue_properties:changed.
Co-authored-by: multica-agent <github@multica.ai>
* feat(web): custom properties settings tab + issue sidebar editors (MUL-4463)
- Settings → Properties: definition management mirroring the Labels tab
(list with type badges/option chips/usage counts, create/edit dialog
with option editor, archive/restore, 20-cap indicator). Admin-gated;
members see a read-only catalog.
- Issue detail sidebar: custom properties join the built-in optional
props' progressive disclosure — set values render as rows with
type-appropriate editors (select/multi-select pickers, calendar,
yes/no, inline input for text/number/url), unset ones live in the
same '+ Add property' menu behind a separator. Archived definitions
render read-only until cleared.
- Core: property types, zod schemas (lenient type strings for forward
compat), api client methods, React Query hooks with optimistic
single-key value writes, ws-updaters + realtime wiring for
property:created/updated and issue_properties:changed.
- Locales: en/zh-Hans/ja/ko strings; Issue fixtures gain properties: {}.
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): address MUL-4463 review round 1 — mobile CI, option guard, mutation safety, schema tolerance
- mobile: EMPTY_ISSUE_FALLBACK gains the required properties field (mobile
typecheck was the red CI check).
- server: PATCH /api/properties/{id} rejects config updates that remove
select options still referenced by issues (409 with a per-option usage
census via jsonb ?); renames keep ids and pass. Integration test included.
- core: property value mutations are serialized per workspace via mutation
scope, snapshot the bag from detail OR list caches (board surfaces have no
detail cache — the old path overwrote whole bags with one key), roll back
to the snapshot or invalidate on error, and the last settled mutation does
an authoritative detail+catalog invalidate (usage counts reconcile).
- schemas: unknown-shaped property values (future server types) are dropped
per-entry in a preprocess step instead of failing the whole IssueSchema
and blanking lists through parseWithFallback; test updated to lock the
tolerant behavior.
- realtime: reconnect invalidation covers the property catalog; every
issue_properties:changed event also refreshes catalog usage counts.
- ui: number editor accepts decimals (step=any); settings usage count
pluralizes (issue/issues) with CJK-safe plural keys.
Co-authored-by: multica-agent <github@multica.ai>
* fix(migrations): renumber issue properties to 179 and build the GIN index concurrently
main's migration sequence advanced twice under this PR (167 collision, then
an upstream renumber wave that claimed 178), so issue properties now sits at
179 — verified against main's current tip by the prefix-uniqueness lint.
The properties GIN index moves to its own single-statement migration (180)
using CREATE INDEX CONCURRENTLY — a plain CREATE INDEX on the hot issue
table would block writes for the duration of the build. Mirrors the
119_user_created_at_index pattern; full-chain dry-run on a fresh database
passes through 180.
Co-authored-by: multica-agent <github@multica.ai>
* feat(web): custom-property list surfaces — filter, cards, sort, board grouping (MUL-4463 M2)
Brings custom properties to the issue list surfaces on top of the M1
definitions/values core:
- Filter: per-definition sections in the Filter dropdown (select /
multi_select options with color dots and counts; checkbox as Yes/No
pseudo-options). OR within a definition, AND across definitions;
client-side in applyIssueFilters, mirrored into filterAssigneeGroups
for the assignee-grouped board. Included in active-filter count and
Clear all.
- Cards: per-property Display toggles (cardPropertyIds) render value
chips on board cards and list rows via CustomPropertyValueDisplay.
- Sort: SortField gains property:<id> for number/date definitions.
Server keeps position order (fixed sort enum); the surface controller
re-sorts client-side, swimlane/gantt reuse the same comparator.
Date-only strings compare lexically; missing values sort last.
- Board grouping: IssueGrouping gains property:<id> for select
definitions — one column per option (definition order) plus a
trailing No-value column, option-colored headings. Drag-drop moves
position via UpdateIssue and applies the value through
useSetIssueProperty/useUnsetIssueProperty (properties are not part
of UpdateIssueRequest). Stale persisted property groupings fall back
to status columns.
View-store: propertyFilters + cardPropertyIds persisted via the
partialize allowlist; clearFilters resets property filters; new fields
deep-merge cleanly into pre-existing persisted snapshots.
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): address MUL-4463 review round 2 — desc sort, option bucketing, archived-state reconciliation
- sort: direction now applies to value comparison only; issues without a
value sort last in BOTH directions (the whole-array reverse flipped them
to the front on desc). Test covers the desc+missing case.
- board: values referencing an option removed from the definition bucket
into the No-value column instead of vanishing (unmatched column ids
dropped the issue entirely). Defense-in-depth behind the new server-side
in-use guard; drag-utils test locks both behaviors.
- controller: persisted propertyFilters keyed by archived/deleted
definitions are stripped before reaching the filter predicates, and a
persisted property sort on a non-active definition degrades to manual
order — previously both kept silently applying while the header claimed
otherwise. The filter badge counts only active-definition filters.
Co-authored-by: multica-agent <github@multica.ai>
* feat(properties): server-side property filtering and sorting on list endpoints
Property filter/sort now execute in the database, so results are correct
across the full issue set — not just the loaded 50-per-status window
(closes MUL-4493 item 1's filter/sort half; requested on MUL-4463).
- New `properties` query param on ListIssues and ListGroupedIssues:
JSON {definitionId: [values]} compiled to an AND-of-ORs containment
check (double NOT EXISTS over jsonb_array_elements). One value expands
to every storage shape it could match — string (select), array element
(multi_select), boolean (checkbox) — so the handler stays type-agnostic.
Guarded at 20 definitions / 50 values.
- `sort=property:<definitionId>` resolves the definition and orders by a
typed expression (numeric CASE cast for number, NULLIF text for
date/text/url); missing values sort last in both directions. Malformed
ids 400; unknown/archived definitions degrade to position order instead
of breaking stale clients.
- Frontend: the property filter and property sort ride the IssueSortParam
window bag, so every surface (workspace + my-issues variants), query
key, and per-status load-more page carries them automatically. The
client-side re-sort layer is gone; applyIssueFilters keeps its property
predicate as an optimistic-update backstop.
- Regression test seeds 55 issues and proves a match at position 55 is
returned by a filtered 50-row page, plus sort order/missing-last,
AND-across-definitions, and the 400/fallback sort paths.
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): address review round 3 — cache reconcile, merged-scope order, GIN-indexable filter, pool loader
- Cache reconciliation: property value writes (mutation settle + WS event)
now invalidate every issue window whose server-side shape depends on
property values — queries filtered by `properties` or sorted by
`property:<id>` (detected via query-key predicate), covering flat lists,
assignee groups, and my-issues variants. Windows without property params
keep the cheap in-place patch. Fixes stale ordering/membership/counts
under staleTime:Infinity.
- My Issues "All" scope: merged assigned/created/involves results are
re-sorted with a comparator mirroring the server ORDER BY semantics
(including property sorts and missing-last, created_at DESC tiebreak) in
both the flat and assignee-grouped merge paths — relation concatenation
no longer overrides the user's sort.
- Filter predicate rebuilt as plain bind-parameter containment ORs
(AND across definitions): EXPLAIN now shows BitmapOr over
idx_issue_properties_gin (the correlated jsonb_array_elements form
defeated the index). Alternatives capped at 256 bind params.
- Property-grouped board gains a pool loader strip: one sentinel per
status that still has server rows, keeping every issue reachable until
per-column pagination lands (MUL-4493).
- Windowing regression test hardened: explicit positions + an assertion
that the unfiltered first page excludes the target (the old fixture tied
at position 0 and the created_at DESC tiebreak put the target on page
one, proving nothing).
- Rollback safety: /api/properties 404 (old server) degrades to an empty
catalog instead of a query error, which also keeps property params from
ever being sent to pre-property servers; migration 179's CHECK
constraints switch to NOT VALID + VALIDATE so the exclusive lock is
instantaneous.
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): harden concurrency and cache coordination from clean-room review
Backend (MUL-4762 F1/F4/F5):
- withPropertyLock: pg_advisory_xact_lock helper; definition create/update
and value writes now serialize config-vs-value and cap-vs-insert races
(workspace-level 'props:' lock + per-definition 'prop:' lock, ordered).
- propertySortExpr degrades archived definitions to position sort.
Frontend (F2/F3/F6):
- onIssuePropertiesChanged invalidates plain assignee-group caches too.
- Property value mutations cancel list refetches in onMutate and roll back
only the touched key against the current bag (concurrent WS writes to
other keys survive a failed write).
- useUpdateIssue reconcile drops the stale properties bag from the server
snapshot; the property pipeline owns that field.
- Surface controller passes persisted property filters/sorts through
until the catalog query settles (cold cache no longer strips them).
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): open_only branch honors the properties filter
ListOpenIssues takes the parsed AND-of-ORs containment groups as a single
jsonb properties_filter param and unrolls them with a static double
NOT EXISTS; previously the open_only path parsed the properties param and
silently dropped it (clean-room review F7a).
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): toast on failed board drag to a property column
Property-column drags rolled the card back silently on failure; mirror
the status/assignee drag path (use-issue-surface-actions) so the
snap-back is explained (clean-room review F3, drag half).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
4fac8d772f |
feat(attribution): Human Attribution Phase 1 (MUL-4302) (#5150)
* feat(attribution): Phase 1 foundation — provenance schema + resolver (MUL-4302)
Human Attribution, Phase 1 (地基) first increment. Every agent run must be
traceable to exactly one accountable human AND record at which waterfall level
that human was resolved, so a NULL originator can be told apart from a genuine
'no human in the chain'.
- migration 149: add originator_source (waterfall label) + delegation/retry/
rerun/rule-version lineage + kind-tagged trigger evidence to agent_task_queue.
No FK, no cascade, no CHECK on the source enum (MUL-4302 §7); nullable ADD
COLUMNs = fast metadata-only change on the hot queue table.
- internal/attribution: the accountable-human vocabulary (Source, EvidenceKind,
TriggerKind) + pure, unit-tested classification rules (ClassifyComment/
ClassifyDirect). No DB, no authorization — provenance labeling only.
- service: attributionFor{IssueTask,TriggerComment} gather facts and delegate to
the pure classifier; the legacy originator resolvers now delegate here so
there is one source of truth. originator_user_id's VALUE is unchanged, so the
Composio-overlay and canInvokeAgent A2A authorization boundaries are
byte-for-byte preserved (MUL-4302 §1.3).
- enqueueIssueTask / enqueueMentionTask stamp originator_source + evidence;
CreateRetryTask carries the parent attribution forward and records
retry_of_task_id so retry and manual rerun stay separable (MUL-4302 §5).
Verified: go build ./..., go vet, gofmt clean; new attribution unit tests +
enqueue stamping integration test green; existing resolve_originator tests
unchanged.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): split accountable_user_id from originator, close enqueue bypasses (MUL-4302)
Phase 1, per Bohan's decision on the MUL-4302 thread: audit and authorization
answer different questions and get different columns.
- Migration 151 adds agent_task_queue.accountable_user_id (no FK, no cascade).
Authorization keeps reading ONLY originator_user_id (canInvokeAgent A2A gate,
Composio overlay); audit/UI/usage read accountable_user_id + source + evidence.
- Invariant (finalizeAttribution, single chokepoint + §11 tests): originator
non-null ⟹ accountable equals it. The two diverge only when originator is null
(autopilot / degraded fallback), which is the deferred rule_owner/owner_fallback
increment; this lands the column + mirror-write so that split has a home.
- Close the NULL-source enqueue bypasses Elon flagged: chat, quick-create,
deferred-fallback and run_only-autopilot now stamp originator_source + evidence
(+ accountable where a human exists). Autopilot stays unattributed until the
rule-version snapshot table lands, but is no longer a silent NULL-source row.
Retry inherits accountable_user_id like the rest of the attribution lineage.
- Fix assign/promote attribution (§4): a member who assigns/promotes an existing
issue is now the accountable human (and, by the invariant, originator) ahead of
the issue creator. Threaded as an OPTIONAL actor override, so comment/rerun/
autopilot paths keep today's resolution and create-with-assignee (creator ==
actor) is unchanged. The squad leader gate already judged the same member.
Also merges origin/main: renumbers the attribution migration 149→150 (main took
149 for issue_origin_agent_create) and folds agent_create into ClassifyDirect's
origin inheritance.
go build/vet/gofmt clean; attribution unit tests + service stamp/actor tests +
handler suite pass on a fresh DB migrated through 151.
Co-authored-by: multica-agent <github@multica.ai>
* docs(attribution): fix accountable NULL semantics + close chat/quick-create evidence boundary (MUL-4302)
Addresses Elon's 2nd-round review on PR #5150 (pre-merge doc/evidence items):
- Migration 151 no longer overclaims NULL. accountable_user_id is NULL not only
on pre-migration rows but on NEW rows whose audit source resolved no human yet
(run_only autopilot writes originator_source='unattributed' with NULL
accountable until rule_owner lands). Header + COMMENT ON COLUMN reworded so a
schema reader does not misjudge the invariant.
- Chat now uses the UNIFORM evidence pair (kind=chat, ref=chat_session_id), like
autopilot_run/issue_assignment, instead of relying only on the dedicated
chat_session_id column — new EvidenceChat kind. Added a service test asserting
chat stamps direct_human + chat evidence.
- Quick-create is documented as the ONE intentional no-antecedent-row path: no
comment/issue/session/run exists at enqueue time (the run creates the issue), so
trigger_evidence_kind/ref stay NULL while the human rides originator/accountable
and source is direct_human — not a NULL-source bypass.
No authorization behavior change. attribution + service + handler suites pass on a
DB migrated through 151.
Co-authored-by: multica-agent <github@multica.ai>
* chore(attribution): renumber migrations 150/151 → 157/158 after merging main (MUL-4302)
main's #5162 ("unblock release migrations") renumbered the chat migrations and
took 150 (agent_task_coalesced_comments) and 151 (chat_read_cursor), colliding
with this branch's attribution migrations. Renumber them above main's new highest
(156) so TestMigrationNumericPrefixesStayUniqueAfterLegacySet passes:
- 150_agent_task_attribution → 157_agent_task_attribution
- 151_agent_task_accountable_user → 158_agent_task_accountable_user
Fixed the internal "migration 150" references in 158's header to 157. Migrations
apply cleanly through 158 on a fresh DB; migration lint green.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): autopilot rule_owner — accountable = rule version publisher (MUL-4302)
Implements rule_owner (MUL-4302 §3.4), the first attribution source where the
accountable human diverges from the (NULL) authorization originator.
- Migration 159 adds the append-only autopilot_rule_version snapshot table (no FK,
no cascade); migration 160 adds its CONCURRENTLY lookup index.
- Write-on-publish: CreateAutopilot appends v1 (publisher = creator); UpdateAutopilot
appends a new version when a SUBSTANTIVE autopilot-row field changes (assignee /
status / execution_mode) — cosmetic edits (title/description/template) write none.
Both run inside the existing handler tx (atomic with the autopilot write).
- Dispatch resolution: both autopilot execution modes now resolve the active rule
version and stamp originator_source='rule_owner', accountable_user_id=publisher,
rule_version_id=<snapshot>, with originator_user_id left NULL (authorization
unchanged). run_only stamps CreateAutopilotTask directly; create_issue resolves in
attributionForIssueTask so both modes attribute identically. A missing version /
non-member publisher degrades to unattributed — never fabricates a human.
- finalizeAttribution now enforces the invariant ONE-WAY: it mirrors originator onto
accountable only when originator is valid, leaving an explicitly-set accountable
(rule_owner / future owner_fallback) intact when originator is NULL. Added
rule_version_id to CreateAgentTask so the create_issue path persists it too.
Also merges origin/main and renumbers this branch's attribution migrations
150/151 → 157/158 (main's #5162 took 150/151); rule_version table is 159/160.
Tests: attribution unit RuleOwner + one-way invariant table; service integration
tests proving an autopilot-origin issue stamps rule_owner + rule_version_id (and
degrades to unattributed with no version). Full service/attribution/handler/
migration suites pass on a DB migrated through 160; build/vet/gofmt clean.
Deferred (same PR): trigger-table republish (cron/webhook/event_filters) and
system-pause/archive versioning; owner_fallback + fail-closed; manual rerun.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): manual autopilot trigger → direct_human to the triggering member (MUL-4302)
Elon's blocking finding: a member manually triggering an autopilot was attributed
rule_owner (accountable = rule publisher, originator NULL) like a schedule/webhook
run, so member B triggering member A's autopilot landed accountable=A and carried
no originator authorization context for the run. Per MUL-4302 §4 a manual "run now"
is a direct human action and must attribute direct_human to the triggering member.
- Thread the triggering member from TriggerAutopilot into dispatch: new
DispatchAutopilotManual carries actorUserID (resolved via resolveActor +
memberActorUserID, so only a member actor is a human; an A2A agent actor falls
back to rule_owner). DispatchAutopilot / DispatchAutopilotForPlan keep their
public signatures (pass an invalid actor); only the internal dispatchAutopilot /
dispatchCreateIssue / dispatchRunOnly gained the param, so the many existing
callers are untouched.
- run_only: dispatchRunOnly stamps direct_human (originator == accountable ==
actor, no rule_version) for a manual actor, else rule_owner. CreateAutopilotTask
gains an originator_user_id param for the manual case.
- create_issue: dispatchCreateIssue enqueues a manual trigger via the actor-carrying
*WithHandoff entry points; attributionForIssueTask's autopilot-origin rule_owner
branch is now guarded on !actorUserID.Valid, so a valid actor falls through to the
direct_human override. Both execution modes attribute identically.
- schedule / webhook keep rule_owner (no actor). Trigger-table + system-pause/archive
versioning remain the pre-merge follow-ups.
Tests: the run_only row assertion Elon asked for (schedule → rule_owner row on
CreateAutopilotTask), plus manual direct_human on BOTH modes (run_only and
create_issue), including a manual actor distinct from the rule publisher. Full
service/attribution/handler/migration/scheduler/cmd suites pass on a DB migrated
through 160; build/vet/gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): owner_fallback + fail-closed policy, and manual-rerun direct_human (MUL-4302)
Two of the three remaining Phase 1 items (trigger-table / system-pause versioning
is deferred — see PR description).
owner_fallback + fail-closed (§1/§3.5) — the never-null accountable guarantee:
- attribution.OwnerFallback degrades an UNATTRIBUTED result to owner_fallback:
accountable = agent owner, originator stays NULL (audit-only, authz untouched),
Source.Precise()==false. finalizeAttribution's one-way invariant already allows
accountable-set / originator-NULL divergence, so nothing else changes.
- Migration 161 adds workspace.attribution_fail_closed (default FALSE) + a lean
GetWorkspaceAttributionFailClosed read. (Also added the column to ListWorkspaces'
explicit column list so its row type stays db.Workspace.)
- applyAttributionFallback is applied at every enqueue boundary (issue, mention,
chat, quick-create, deferred-fallback, autopilot run_only): unattributed →
owner_fallback (agent owner) by default, or ErrAttributionFailClosed when the
workspace is fail-closed, which the caller surfaces to refuse the enqueue (the
run does not start). So no run is left without an accountable human, and a
compliance workspace can block unattributable runs instead.
manual rerun (§5) — a rerun is a NEW direct_human trigger to the rerunning member:
- RerunIssue threads the acting member (resolved in the handler via resolveActor)
down to enqueueRerunTask, and attributionForIssueTask is now actor-first so the
actor wins over an INHERITED trigger comment (a rerun keeps the comment for the
daemon's prompt context but must attribute to whoever clicked rerun, not the
original comment's human).
- rerun_of_task_id lineage is recorded via a targeted SetAgentTaskRerunOf update on
the rerun path only (keeping the shared CreateAgentTask insert untouched), so
system retry (retry_of_task_id) and human rerun stay separable in reporting.
Tests: OwnerFallback unit test; owner_fallback + fail-closed-refusal + manual-rerun
(direct_human + rerun_of_task_id) service tests; the prior "degrades to unattributed"
test updated to owner_fallback. Full service/attribution/handler/migration/scheduler/
cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): close fail-open holes + move rerun_of_task_id into creation snapshot (MUL-4302)
Addresses Elon's two must-fixes on PR #5150.
1. accountable-never-null / fail-closed had fail-open holes. applyAttributionFallback
now, for an UNATTRIBUTED run, refuses the enqueue (ErrAttributionFailClosed) in
THREE cases instead of silently degrading to a runnable NULL-accountable task:
- workspace policy read fails (or no workspace) → fail closed; we cannot confirm
fallback is permitted, so we don't run an unattributable task on a DB hiccup.
(Only the rare unattributed path pays this; precise runs never read the policy.)
- workspace is fail-closed → refuse (unchanged).
- owner_fallback has no valid agent owner → refuse rather than enqueue a task with
a NULL accountable_user_id.
ErrAttributionFailClosed's doc now covers all three "cannot guarantee an
accountable human" refusals. Added missing-owner / policy-read-failure /
precise-passthrough tests.
2. manual rerun rerun_of_task_id was a post-notify UPDATE (race: the queued event /
daemon claim could see rerun_of_task_id = NULL, and a failed update degraded the
run to a plain direct_human). It now rides the CreateAgentTask insert — threaded
through enqueueIssueTask / enqueueMentionTask as a creation param (like
retry_of_task_id) so it is written in the same statement before the daemon is
notified. Removed the SetAgentTaskRerunOf follow-up query.
Also merges origin/main (unrelated CLI fix #5167, no conflict). Full service /
attribution / handler / migration / scheduler / cmd suites pass on a DB migrated
through 161; build / vet / gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): rule_owner versioning on trigger edits + system-pause/archive (MUL-4302)
The final remaining Phase 1 item: substantive publishes beyond the autopilot row now
republish the rule version, so a run's rule_owner accountable follows whoever last
changed what the rule does.
- Extracted the config-summary + insert into service.RecordAutopilotRuleVersion so
the handler and the (different-package) failure monitor share one writer; the
handler's recordAutopilotRuleVersion is now a thin wrapper.
- Trigger edits: UpdateAutopilotTrigger and DeleteAutopilotTrigger republish the rule
version with the acting member as publisher, ATOMICALLY (tx-wrapped mutation +
version write, mirroring CreateAutopilot/UpdateAutopilot). CreateAutopilotTrigger
republishes best-effort — the webhook path mints its token with a retry loop that
cannot share one tx, and a create is usually initial setup already covered by v1;
a failed write there is benign (active version stays the current publisher, the new
trigger fires under it, no immediate daemon claim rides it).
- Archive (DeleteAutopilot) republishes (member, status=archived), tx-wrapped.
- System auto-pause (failure monitor) republishes with a 'system' publisher,
best-effort — a background sweep to a non-dispatching state (a paused autopilot
never dispatches; a later member resume supersedes).
- RotateWebhookToken / SetSigningSecret deliberately do NOT version: they rotate
credentials, not the rule's behavior (not §3.4 substantive).
Semantics: a system-published (no-member) active version degrades dispatch to
unattributed → owner_fallback, never fabricating a human.
Tests: republish-reattributes (member A → member B supersedes → dispatch resolves to
B; system publisher → unattributed). Full service/attribution/handler/migration/
scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Also merges origin/main (unrelated frontend feature #5074).
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): make trigger-create rule-version republish atomic (MUL-4302)
Addresses Elon's final Phase 1 blocking finding: CreateAutopilotTrigger recorded the
rule-version republish best-effort AFTER the trigger insert. If member B added a
schedule/webhook trigger to member A's autopilot and the version write failed, future
schedule/webhook dispatches would keep attributing to A — violating the rule_owner
invariant that the last member to substantively change the rule owns future runs
("no immediate daemon claim" doesn't save it, since the miss surfaces at the LATER
trigger firing).
Both create paths now write the version in the SAME tx as the trigger INSERT:
- schedule create: wrap CreateAutopilotTrigger + recordAutopilotRuleVersion in one tx.
- webhook create: each mint-with-retry attempt runs in its own tx (insert + version
commit together; a token collision rolls that attempt back and retries with a fresh
token; a version-write failure rolls the trigger back). Passes ap + the acting
member id into the helper.
- removed the best-effort recordTriggerRuleVersionBestEffort helper (and the now-unused
slog import).
Test: TestCreateTrigger_RepublishesRuleVersionAtomically drives both create paths
through the handler and asserts a rule version is published by the acting member.
Existing webhook/trigger/archive handler tests still pass. Also merges origin/main
(unrelated avatar feature #5074). Full service/attribution/handler/migration/
scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): Phase 2.1 — surface run attribution on the task API (MUL-4302 §9)
First Phase 2 (visibility) increment: the agent-task API now returns the resolved
accountable-human provenance so the UI can render an "on behalf of" badge.
- AgentTaskResponse gains an `attribution` object: source label (never blank —
pre-migration NULL renders "unattributed") + `precise` flag (false for the degraded
owner_fallback / backfill / unattributed sources), the initiator (accountable) and
originator (authorization) user refs, the evidence {kind, ref_id} pointer, and the
rule_version / delegated / retry / rerun lineage ids.
- The label + evidence + raw ids are built in the PURE taskToResponse (no DB), so
every task response carries them. Names are hydrated separately, only on the
user-facing surfaces (ListAgentTasks, ListWorkspaceAgentTaskSnapshot, RerunIssue,
CancelTaskByUser) — daemon-claim paths stay lean.
- Hydration resolves initiator/originator from the GLOBAL user table (departed-member
safe) via a new batch GetUsersByIDs query (no N+1); best-effort, so a lookup hiccup
leaves the raw ids intact.
Tests: pure taskAttributionBase (direct_human / rule_owner NULL-originator /
owner_fallback degraded / pre-migration→unattributed) + DB hydration (fills known
ref, leaves unknown id un-filled, skips nil). Full handler/service/attribution/
migration/scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt
clean. The field is additive — the frontend's parseWithFallback ignores unknown keys,
so nothing breaks until the UI increment consumes it.
Also merges origin/main (unrelated editor feature #5090).
Remaining Phase 2 (next increments, same PR): frontend zod schema + "on behalf of"
badge + evidence-chain jump; append-only correction events (write + display).
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): Phase 2.2 — on-behalf-of badge in the execution log (MUL-4302 §9)
Surface the accountable human on every agent run row:
- AttributionBadge composes Badge + ActorAvatar, shows "on behalf of <member>"
with the resolution source as a tooltip; degraded (non-precise) attribution
gets a warning tone, and an unresolved initiator renders an explicit
"no responsible member" chip.
- Wire the badge into both active and past rows of the execution log.
- Mirror the attribution shape into AgentTaskResponseSchema (defensive, .loose())
so the cancel-task path carries it through zod; add parse tests.
- Export TaskAttribution/AttributionUser/TaskEvidence from @multica/core/types
and add the attribution block to all four issues.json locales.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): hydrate initiator names on issue-facing task endpoints + bound the badge (MUL-4302 §9)
Address Elon's PR #5150 review:
- ListTasksByIssue (the execution-log data source), GetActiveTaskForIssue and the
issue-scoped CancelTask now call hydrateTaskAttributions, so the "on behalf of
<member>" badge shows the real member name on issue detail instead of falling
back to "someone". Mirrors the existing ListAgentTasks / snapshot behavior.
- AttributionBadge: cap width (max-w-40, min-w-0) and truncate the name span so a
long name / narrow right column can't squeeze out trigger/status/actions; keep
the avatar shrink-0.
- Add a handler test asserting the issue task list returns a hydrated
attribution.initiator.name.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): use semantic AvatarSize 'xs' for the badge avatar
main refactored ActorAvatar.size from a raw pixel number to the semantic
AvatarSize union (packages/ui/lib/avatar-size). Switch the on-behalf-of badge
avatar from size={14} to size="xs" (16px) after merging main.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): stage-cascade falls back to parent-issue provenance, not agent owner (MUL-4302)
When closing the last sub-issue in a Stage wakes the parent's assignee agent, the
run was enqueued via a system-authored child-done comment with no actor, which the
resolver classified as unattributed and then degraded to owner_fallback (the agent's
own owner). That is the wrong accountable human: the woken run should be accountable
to whoever caused the parent issue to exist.
attributionForIssueTask now detects a system-authored trigger comment and falls
through to the parent issue's own provenance — the same creator / agent_create-origin
/ autopilot-origin chain a direct enqueue resolves (so an agent-decomposed parent
attributes via delegation to the human who drove it; a member-created parent to that
member; an autopilot parent to the rule publisher). owner_fallback is now only the
last resort when the parent provenance itself has no human.
- Extract attributionFromComment so attributionForIssueTask can inspect author_type
without a second GetComment; authorization resolution stays byte-identical.
- Add a DB-backed test asserting a system child-done comment resolves to the parent
issue's origin human (delegation), not owner_fallback.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): autopilot runs attribute to the firing trigger's creator (MUL-4302)
Per Bohan: an autopilot schedule/webhook run should be accountable to the human
who created the SPECIFIC trigger that fired it, not the rule publisher. (Manual
triggers already attribute to the invoking member via direct_human — unchanged.)
- Migration 162: add autopilot_trigger.created_by_type/created_by_id (nullable, no
FK/cascade). Capture the creating member at both trigger-create sites (schedule +
webhook).
- New precise source trigger_owner: originator stays NULL (an autonomous fire
carries no human authorization — same authz-safe divergence as rule_owner),
accountable = the trigger's member creator.
- triggerOwnerAttribution resolves run.trigger_id → creator; wired into run_only
dispatch and the create_issue path (bridging issue → active run → trigger_id).
Legacy triggers with no recorded creator, and agent-created triggers, degrade to
rule_owner then owner_fallback — nothing regresses.
- Frontend: trigger_owner source label in all four locales + badge switch case.
- Tests: attribution TriggerOwner unit + Precise/invariant; DB-backed resolver
tests (member creator → trigger_owner; creatorless → rule_owner fallback).
Co-authored-by: multica-agent <github@multica.ai>
* chore(attribution): re-trigger CI (dropped synchronize event on
|
||
|
|
9eddcaff10 |
fix(chat): defer cancellation-time finalization until the task transcript is stable (#5246)
A quick Stop before the agent's first token no longer races a late reply. Started-but-empty cancellations defer the empty/non-empty judgment until the daemon acks its transcript flush (or a grace-period sweeper fires), then settle to a single outcome. Empty outcomes persist a durable, creator-authorized draft restore (fetched/consumed via a dedicated endpoint, reconnect-safe and at-most-once) instead of broadcasting the prompt over the workspace bus. Closes #5219 |
||
|
|
7985699df9 |
feat(help): surface the running server version in the Help popover (#4959)
Surface the running server build version in the Help popover so self-hosted operators can confirm what's deployed and include it in bug reports. - Backend exposes it via /api/config's server_version (from main.version), omitempty so older/unstamped builds omit the field. - Unstamped "dev" builds are normalized to empty and the row stays hidden. - The row is suppressed on the managed cloud (frontend host multica.ai) and shown only on self-hosted deployments. - Frontend renders a muted footer row in the Help popover only when the value is non-empty; i18n added for en/ja/ko/zh-Hans. |
||
|
|
6caa3397ea |
fix(comment): sanitize NUL/invalid bytes in comment content and trigger preview (#5391)
Agent-written comment content can carry a byte PostgreSQL's TEXT column rejects -- reachably an embedded NUL (0x00, SQLSTATE 22021) that survives a JSON round trip. CreateComment/UpdateComment inserted content verbatim, so such a byte failed the INSERT with an opaque 500 the CLI renders as "server unavailable" and retries. This is the plausible cause of #5388 (a large --content-file body erroring while shell-re-encoded --content-stdin succeeded); the reporter's exact bytes are unconfirmed. - Apply the existing sanitizeNullBytes() helper (already used on skill import for this PG failure mode) to comment content before the empty check, in CreateComment and UpdateComment. - Apply the SAME normalization in PreviewCommentTriggers so the trigger preview and the real create/update side effect agree; otherwise a mention hidden behind a NUL reads as inert in preview but enqueues the agent on submit. - Tests: NUL content posts (201, byte stripped) instead of 500; the preview target set matches the enqueue set for a NUL-hidden mention on both the create and edit paths. Refs #5388 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6b4d690664 |
MUL-4744: restore Autopilot webhook response contract (#5397)
* fix(autopilots): restore webhook response contract Co-authored-by: multica-agent <github@multica.ai> * fix(autopilots): preserve run id on webhook retries Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e07b5403ab |
MUL-4502: make autopilot webhook admission durable (#5386)
* fix(autopilots): make webhook admission durable Co-authored-by: multica-agent <github@multica.ai> * fix(autopilots): address webhook delivery review Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6c6143e8fc |
fix(agents): always enable skill toggles (MUL-4520) (#5381)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
af9d90bd83 |
fix(daemon): wake queued tasks after predecessor exits (#5379)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ac62f72c2a |
MUL-4480: make daemon workspace sync event-driven (#5354)
* feat(daemon): make workspace sync event-driven Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): preserve trailing workspace changes Co-authored-by: multica-agent <github@multica.ai> * fix(workspace): reconcile failed creates Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c3dd9ec845 |
Machine-level batch task claim endpoint (MUL-4257) (#5193)
* feat(daemon-claim): machine-level batch task claim endpoint (MUL-4257) Collapse the per-runtime /tasks/claim poll fan-out into a single machine-level batch claim to cut /api/daemon claim request volume. Server: - agent.sql: = ANY(runtime_ids) batch variants of the claim queries (ListQueuedClaimCandidatesByRuntimes, PromoteDueDeferredTasksForRuntimes, ReclaimStaleDispatchedTasksForRuntimes); runtime.sql: GetAgentRuntimes(= ANY) so a whole machine's runtimes are resolved/promoted/reclaimed/listed in a constant number of queries instead of N. - service.ClaimTasksForRuntimes: claim up to max_tasks across a runtime set, preserving per-(issue,agent) serialization, the concurrency cap, the empty-claim cache short-circuit, and every dispatch side effect. Batch promote replays the per-row side effects (task:queued + empty-cache Bump). - handler.ClaimTasksByRuntime (canonical POST /api/daemon/tasks/claim, with a transitional /claim alias): validates daemon_id (required; must match the mdt_ token) and rejects runtimes bound to a different daemon (group-ownership check mirroring the WS path); resolves+authorizes each runtime_id; claims; and finalizes each task through the SAME FinalizeTaskClaim as the per-runtime endpoint (atomic token + delivered_comment_ids receipt), requeueing the exact claim and omitting it on failure. buildClaimedTaskResponse is extracted from the per-runtime handler and returns the delivered-comment ids plus a structured *claimBuildFailure so both paths share identical payload building and failure semantics (workspace-isolation, chat-input load/empty). - max_tasks: negative -> 400, zero -> empty (never coerce to 1), positive capped at 32. runtime_ids parsed with non-panicking util.ParseUUID. Daemon: - Client.ClaimTasks posts daemon_id + runtime set + free-slot count to the canonical path under a short request-scoped timeout, bounding the head-of-line coupling the per-runtime pollers avoid (MUL-1744). Tests: service batch drain / max_tasks cap / deferred-promote receipt / finalize-failure rollback+requeue; handler routing + token, cross-workspace skip, cross-daemon skip, daemon_id required, owner-missing cancel, max_tasks=0/negative, invalid-uuid skip, comment delivery receipt, stale-reclaim replacement receipt; client posts/parses (daemon_id + canonical path). Follow-up: cut the daemon pollLoop over to a single batched poller (flips the MUL-1744 isolation contract; needs its concurrency tests redesigned). Co-authored-by: multica-agent <github@multica.ai> * feat(daemon-ws): generic WS request/response transport for daemon RPC (MUL-4257) Add a generic daemon->server request/response layer over the existing WS control connection, the transport for WS-first claim (HTTP fallback): - protocol: daemon:rpc_request / daemon:rpc_response envelopes with a correlation request_id + method + body, and an rpc-v1 capability gate. - daemonws.Hub: SetRPCHandler + goroutine-dispatched handleRPCFrame (bounded by a per-connection in-flight cap) that echoes the request_id; missing handler / saturation return non-2xx so the daemon falls back to HTTP. Read limit raised to 64KB for rpc requests carrying a runtime set. - hub tests: round-trip, handler-error->non-2xx, no-handler->503. Co-authored-by: multica-agent <github@multica.ai> * feat(daemon-ws): WS-first task claim over the generic RPC transport (MUL-4257) Bind claim to the WS request/response layer, with HTTP fallback: - server: handler.DaemonRPCHandler adapts a daemon:rpc_request (method tasks.claim) to the existing HTTP ClaimTasksByRuntime via a synthetic in-process request carrying the WS connection's identity (daemon_id + workspace + capabilities), so all auth / payload-building / finalization is reused unchanged. Wired via daemonHub.SetRPCHandler. ClientIdentity now captures X-Client-Capabilities so capability gating matches the HTTP path. - daemon: wsRPCClient correlates responses by request_id over the shared WS connection; attached to the live connection's write channel (guarded so a Call racing teardown never sends on a closed channel) and detached on disconnect. rpc_response frames are routed in the read loop. Daemon.ClaimTasksWSFirst issues tasks.claim over WS and falls back to the HTTP claim endpoint on any transport failure (no conn / buffer full / timeout) — wired into the poller at the poller cutover. - tests: handler tasks.claim RPC end-to-end (claims + dispatches) + unknown method 404; daemon wsRPCClient round-trip / timeout / unavailable / server-error / detach-fails-pending (all under -race). Co-authored-by: multica-agent <github@multica.ai> * feat(daemon): cut claim poller over to machine-level ClaimTasksWSFirst (MUL-4257) Replace the per-runtime HTTP poll loop with a single batch poller: each cycle acquires all free execution slots (slot-before-claim) and issues ONE ClaimTasksWSFirst across every runtime the daemon hosts (WS-first, HTTP fallback), dispatching each returned task to its runtime. Wakeups (targeted / catch-up / runtime-set change) collapse to one nudge. Removes runRuntimePoller + runtimePollOffset. The WS handshake now advertises the same capabilities as HTTP (+ rpc-v1) so WS-built claim payloads keep skill-ref / coalesced-comment gating. Trades per-runtime isolation (MUL-1744) for one request, bounded by the short per-request WS timeout / client timeout. Tests: batch poller claims across runtimes + skips-at-capacity + pollLoop shutdown drain (replacing the per-runtime poller tests); heartbeat isolation + runtime-set watcher kept. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon-ws): WS RPC disconnect-race panic + batch stale-comment-plan repair (MUL-4257) Two PR #5193 review blockers: 1) WS RPC send-on-closed-channel race, both ends: - server: give each connection a cancelable ctx (cancelled on readPump teardown) and run the RPC handler under it, so a slow claim stops on disconnect; guard c.send with sendMu/sendClosed (trySend) so a late RPC response goroutine never writes to the closed channel. Heartbeat ack routed through the same guard. - daemon: wsRPCClient.deliver now sends under the mutex, serialized with attach(nil)'s close+delete, so a delivered response can't hit a channel the detach path just closed. - regressions (-race): daemon deliver-vs-detach; server disconnect-during-handler-response. 2) batch claim now runs the stale-comment-plan repair: extracted the per-runtime handler's repair (trigger deleted, only coalesced survive -> cancel + replay survivors) into shared repairStaleCommentPlanIfNeeded, called by both claim paths. Prevents the batch path (now the default poller) from finalizing+dispatching a task with no comment input and silently dropping the surviving user comment. Regression: batch omits the stale task, cancels it, and rebuilds the survivor into a new trigger plan. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon-ws): server-side RPC deadline + legacy claim fallback (MUL-4257) Two review blockers: 1) WS RPC timeout/fallback (GPT-Boy): the daemon's WS wait didn't cancel server-side claim, so a slow WS claim could commit after the daemon fell back to HTTP, leaking dispatched tasks and breaking the free-slot bound. Fix: RPC envelope carries TimeoutMs; the server bounds the handler ctx by it (so ClaimTasksByRuntime's tx is cancelled/rolled back at the deadline), and the daemon waits budget + grace so a claim that committed before the deadline still reports back. A committed-then-unreported claim degrades to the same stale-reclaim safety net as HTTP, never a double effective claim. Regression: server-side TimeoutMs cancels the handler. 2) Backward compat (Terra-Boy): a new daemon against a server without the batch route (/api/daemon/tasks/claim 404) couldn't claim. Fix: ClaimTasksWSFirst falls back to the legacy per-runtime ClaimTask loop on a batch 404 and caches 'batch unsupported' (reset on WS reconnect to re-probe after a server upgrade). Regression: server exposing only the legacy route. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon-ws): no double-claim on WS teardown/detach (MUL-4257) Sol-Boy review blocker: on reconnect, teardown failed the pending RPC (→ HTTP fallback) but then flushed the queued tasks.claim frame to the still-alive socket, so the server committed the WS claim on top of the HTTP one — double claim, WS batch orphaned to stale reclaim, breaking the free-slot bound. - Teardown now closes the connection FIRST, so runWSWriter discards the queued RPC frame (write error path) instead of delivering it. - A detach while a claim's frame is already in flight now returns a distinct errWSRPCUncertain; ClaimTasksWSFirst does NOT HTTP-fall-back on uncertain (the WS claim may have committed) — it skips the cycle and lets reclaim / the next poll recover. Genuine 'not sent' / timeout still fall back (safe: the server-side deadline guarantees no uncommitted claim by budget+grace). - Regression: detach during an in-flight WS claim asserts zero HTTP claims (at most one path claims); plus the existing detach/deliver-race and server-timeout tests. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon-ws): cancelable RPC frames close the backpressure double-claim (MUL-4257) Sol-Boy review blocker: the client's response budget starts at enqueue, but the socket write is async (10s write deadline). A backpressured writer could hold a tasks.claim in the local queue past the client timeout — the daemon HTTP-fell-back, then the writer woke and delivered the stale WS frame, so the server committed it too: same free slots claimed twice. No detach occurs, so the prior errWSRPCUncertain fix did not cover it. - WS frames are now cancelable (wsOutbound{sent,canceled} under a mutex). The writer calls beginWrite() before WriteMessage and skips cancelled frames. - On give-up (timeout / detach / ctx), Call cancels the queued frame: if it was still pending the cancel wins and the frame is guaranteed never delivered (errWSRPCUnavailable → safe HTTP fallback); if the writer already began sending it the cancel loses and the outcome is errWSRPCUncertain (no fallback). The decision is atomic, so at most one transport claims. Tests: wsOutbound cancel-before-write vs write-before-cancel; Call timeout cancels an unsent frame (writer then drops it) vs uncertain when already sent; plus the updated detach and existing timeout/race tests. Co-authored-by: multica-agent <github@multica.ai> * fix(batch-claim): return partial success instead of dropping committed claims (MUL-4257) Sol-Boy review blocker: ClaimTasksForRuntimes reclaims (step 2) and claims per agent (step 6) in independent transactions, but a step-4 candidate-SELECT error or a mid-loop ClaimTask error did 'return nil, err' — discarding tasks already committed as dispatched. The handler 500s; the daemon sees a definite (non- uncertain) 500 and HTTP-falls-back, claiming a SECOND batch into the same free slots while the first batch waits for stale reclaim — the double-claim this PR removes. - Both error paths now prefer partial success: if any task has already committed (claimed non-empty), return it (nil error) so the handler finalizes and returns 200; the errored candidates stay queued for the next poll. The remaining error is logged. Only a genuinely empty result still returns the error (safe: no committed claim to lose, HTTP fallback just re-fails). Regression (internal/service, DB-backed, fault-injected): - PartialSuccessOnSecondAgentClaimFailure: fail the 2nd ClaimTask's Begin → the first agent's committed task is returned, not dropped. - PartialSuccessOnCandidateQueryFailureAfterReclaim: a stale dispatched task is reclaimed, then the candidate SELECT fails → the reclaimed task is returned. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
47f9c5813f |
fix(chat): reland archived-session unread + mount-race auto-read (MUL-4360) (#5333)
* fix(chat): correct unread — archived sessions + mount-race auto-read (MUL-4360) Reland of #5315, which was reverted by #5332 as collateral in an unrelated release-wide revert (to unwind 162/163 migration BLOCK from other PRs), not for any defect in this code — Howard/Preflight assessed these changes WARN / non-blocking. Restores all three fixes verbatim off current main: - backend: ListAllChatSessionsByCreator derives unread_count=0 / has_unread=false for status='archived' rows via a CASE gate. last_read_at is untouched, so unarchiving restores the true unread state. Single source of truth for every unread surface (quick-chat FAB, sidebar Chat tab, chat-window header, mobile); installed desktop clients benefit with no app update. - frontend: the archive mutation onMutate optimistically zeroes the row's unread so no badge counts a just-archived session in the frame before the refetch lands. Unarchive does not fabricate a count — the true state returns from the server refetch. - frontend: auto-mark-read is deferred a tick and cancelled on cleanup, so a session that is only momentarily active on mount (persisted activeSessionId restored for one frame, then cleared by the URL->store effect) is not marked read; only a session that stays active past the tick is. Verification: sqlc regenerate produces no drift; go test ./internal/handler -run 'TestListChatSessions_ArchivedSessionReportsZeroUnread|TestSetChatSessionArchived_ClearsChannelBinding' passes against a real Postgres; vitest mutations.test.tsx (3) and use-chat-controller.test.tsx (8) pass; core + views typecheck clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): converge archived unread on chat:session_updated realtime event (MUL-4360) Howard's #5333 review found a real cross-tab gap: the chat:session_updated handler patched status but never unread, and chatSessionsOptions is staleTime: Infinity, so a session archived in one tab kept its unread badge lit in another tab/device forever — the same stuck-badge bug, one surface over. Extract the inline handler into applyChatSessionUpdatedToCache and force unread_count=0 / has_unread=false when payload.status === "archived", mirroring the archive mutation's optimistic patch and the backend deriving unread=0 for archived rows. Unarchive does NOT fabricate a count — the true unread returns from the server refetch (last_read_at untouched). No sessions-list invalidation; minimal field patch as reviewed. Adds use-realtime-sync.test.ts coverage: an archived event zeroes a cached unread row; an active event does not resurrect unread; a rename-only event leaves unread untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
45ff984518 |
fix(labels): sweep resource-label junctions on bulk deletes (MUL-4479) (#5348)
* fix(labels): sweep resource-label junctions on bulk deletes (MUL-4479) Runtime, runtime-profile, and workspace deletion hard-delete their agents and skills without clearing agent_to_label / skill_to_label. Migration 173 dropped the junction foreign keys, so these rows are no longer cascade-cleaned; once resource labels are enabled, every labelled agent/skill removed through one of these bulk paths leaves a permanent, invisible orphan junction row. Clear the junctions in the same transaction as the owner delete, before the owning rows disappear: - DeleteAgentRuntime / ArchiveAgentsAndDeleteRuntime / DeleteRuntimeProfile: DeleteAgentLabelAssignmentsByRuntime, ahead of the archived-agent hard-delete. - DeleteWorkspace: DeleteAgentLabelAssignmentsByWorkspace + DeleteSkillLabelAssignmentsByWorkspace, ahead of the workspace cascade. Adds regression tests for all four delete paths. Follow-up to #5345 (Elon review). Resource labels must stay disabled until this lands. Co-authored-by: multica-agent <github@multica.ai> * fix(labels): make workspace cleanup atomic Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Eve <eve@multica-ai.local> |
||
|
|
411a160b99 |
fix(release): harden v0.3.44 migrations (#5345)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
220fa58264 |
fix: guide SSH installs to token login (#5318)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9a071b827f |
Revert "fix(chat): correct unread — archived sessions + mount-race auto-read …" (#5332)
This reverts commit
|
||
|
|
79c4589f7d |
fix(server): don't cancel active tasks when issue status → cancelled (#5328)
Moving an issue to `cancelled` used to auto-cancel every in-flight agent task on that issue. Users have no expectation that clicking "cancel" stops running agent runs, so this implicit coupling is removed from UpdateIssue and BatchUpdateIssues. Deleting an issue still cancels its tasks (the owning row disappears); a plain status change never does. Reassignment already didn't cancel tasks (#4963 / MUL-4113), so this makes status-cancel consistent. - Status-table-driven regression tests cover every active state the cancel query sweeps (queued / dispatched / running / waiting_local_directory / deferred) on both the single and batch paths. - Updated the multica-working-on-issues skill (SKILL.md + source map) and corrected stale comments in task.go and agent.sql that described the removed coupling as current. MUL-4465 |
||
|
|
cec071dc06 |
fix(chat): correct unread — archived sessions + mount-race auto-read (MUL-4360) (#5315)
* feat(skills): search runtime local skills * feat(skills): highlight matched substrings in runtime local skill search Reuse the shared HighlightText (the same component the global search command uses) to highlight matched substrings in a result's name, provider, description, and path, so styling stays consistent across the app. Narrow the search to the fields the row actually renders and drop `key`, so every match maps to something visible. While a query is active, lift the description's 2-line clamp so a match past the first two lines stays on screen instead of being clipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): zero unread for archived chat sessions across all badges (MUL-4360) Archiving a chat session flips status but deliberately does not advance last_read_at, and ListAllChatSessionsByCreator counted unread unconditionally. So an archived session that had unread replies kept reporting has_unread=true / unread_count>0 — a stuck badge the user can never clear (archived sessions are read-only and hidden from history, so there is no mark-as-read entry). MUL-4372 fixed only the quick-chat FAB surface; the sidebar Chat tab badge and the chat-window "other unread" header still counted it. Fix at the source: derive unread_count = 0 for status='archived' rows in ListAllChatSessionsByCreator. Because has_unread is server-derived as unread_count > 0, and all surfaces (FAB, sidebar via countUnreadChatMessages, chat-window header, and mobile) read this one payload, every badge drops archived sessions with no per-surface filter. last_read_at is left untouched so unarchiving restores the true unread state. Installed desktop clients benefit without an app update. Also zero unread optimistically in the archive mutation so no badge counts a just-archived session in the frame before the refetch lands (FAB already filtered archived; this keeps sidebar/header consistent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): stop auto-mark-read from clearing a transiently-active session on mount (MUL-4360) The chat page persists `activeSessionId`, so on a bare `/chat` navigation it restores the last-open session as active for one frame before its URL→store effect (which runs AFTER useChatController's effects, since the hook is called first) clears it back to null. The auto-mark-read effect fired in that gap and marked the restored-but-never-opened session read — its unread badge vanished though the right pane still showed "select a chat" and the user never opened it. This is why the sidebar count dropped (e.g. 2 → 1) just by entering the tab. Defer the read by a tick and cancel it on cleanup: a session that is only momentarily active (restored on mount, then cleared) has its pending read cancelled when activeSessionId changes; only a session that stays active past the tick — a real select, deep link, or refresh — is marked read. A live-store re-check in the timer is a belt-and-suspenders guard. Adds the previously-missing auto-mark-read coverage: a stable-active session is read after the tick; a momentarily-active one is not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: coderbaozi <YHbaozi1988@163.com> Co-authored-by: abun <103836393+coderbaozi@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
eacc842280 | fix(agents): constrain builder models to runtime (#5323) | ||
|
|
bf288349f6 |
feat(project): add start_date and due_date fields (MUL-4388) (#5313)
Projects become schedulable planning objects alongside their issues: add optional start_date / due_date, mirroring issue.start_date / issue.due_date. This is only the first slice of #5227 — labels, metadata, and the editable metadata UI are still out of scope. - migration 166: two nullable DATE columns on `project` (calendar days, no FK/index — matches the issue end-state after migration 112) - sqlc CreateProject / UpdateProject carry the dates; UpdateProject uses narg so an explicit null clears - handler: parse YYYY-MM-DD (400 on bad format), rawFields-presence clear on update, and the hand-scanned SearchProjects query returns the columns - CLI: `project create/update --start-date/--due-date` (empty clears on update) - frontend + mobile types/zod schemas: the two new schema fields are nullable().default(null) so a project from an older backend (frontend deploys before backend) parses to null instead of degrading the batch to the empty fallback; added a search schema drift test - projects skill / CLI docs Part of #5227 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4ee34b9fcb |
fix: return stage from issue list APIs
Closes MUL-4393 |
||
|
|
0b054b5c8b |
fix(chat): clear external channel binding on archive, drop archived from FAB unread (#5214)
Archiving a channel-bound chat session now severs its channel_chat_session_binding in the same tx as the status flip. The web send path already treats status='archived' as read-only, but the channel engine (Feishu/Slack) resolves inbound traffic through the binding without checking session status, so an archived-but-still-bound session kept receiving agent replies and a stuck, uncleared unread badge. Dropping the binding makes the next inbound message fork a fresh session; unarchive does NOT recreate it (a later session may already own the channel). The FAB unread badge counted status=all sessions without excluding archived, so residual unread on an archived session (archive does not mark-read) held the badge even though the session is hidden and read-only. Extract countUnreadChatSessions() and exclude archived. This matches what mobile already computes (active-only list), restoring count parity. Tests: backend archive-clears-binding + unarchive-does-not-recreate; frontend countUnreadChatSessions archived-exclusion cases. MUL-4372 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a19e60a9e6 |
feat(chat): support images/files in agent chat replies (MUL-4287) (#5164)
* feat(chat): support images/files in agent chat replies (MUL-4287) Agents can now attach images/files to their chat replies, matching how comment attachments already work. The write-side gap was that the assistant chat_message is synthesized server-side from the completion callback's text output and never bound any attachments. Backend: - migration 150: nullable attachment.task_id (+ partial index), the transient handle that ties an agent's in-run upload to the reply it produces. - POST /api/upload-file accepts task_id: gated to the task's own agent, in this workspace, on a chat task; tags the row with task_id + chat_session_id. - CompleteTask (chat branch) binds the task's still-unclaimed attachments to the assistant message via BindChatAttachmentsToMessage (rejects rows already owned by an issue/comment/chat_message). An empty-output reply that produced files still creates a message so the images have an owner. FailTask binds nothing. CLI: - `multica attachment upload <path>` uploads a file for the current chat task (task from MULTICA_TASK_ID or --task) and prints id / markdown_url / a ready-to-paste markdown snippet. Prompt: - web/mobile chat prompt tells the agent how to attach a file to its reply. Mobile: - chat:done handler now always invalidates the messages list so attachments (absent from the event payload) refetch; mirrors web's self-heal. - chat bubbles render standalone attachment cards via the existing CommentAttachmentList (dedup vs inline references), matching web. Web/desktop needed no change — they already render message.attachments inline and via AttachmentList, and self-heal on chat:done. Tests: upload permission/isolation, bind-on-complete, empty-output+attachments, FailTask no-bind, null task_id untouched, already-owned not stolen, CLI output contract, mobile refetch-on-done. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address review blockers on chat reply attachments (MUL-4287) Two final-review blockers on PR #5164: 1. Mobile inline dedup only checked raw `url`, so an attachment referenced inline via `markdown_url` (exactly what the CLI snippet emits) rendered twice — once inline, once as a standalone card. Reuse the core `contentReferencesAttachment` helper so dedup covers every real reference form (stable /api/attachments/<id>/download path, url, download_url, markdown_url), matching web's AttachmentList. Extracted the filter into a pure `lib/attachment-dedup.ts` so it is unit-testable, and added a regression test covering `content` containing `attachment.markdown_url` (plus the other URL forms and same-identity sibling dedup). 2. CLI `attachment upload` emitted `![...]` image markdown for every file, producing a broken-image snippet for non-images. Emit image markdown only for image/* content types and a plain link otherwise, with a CLI contract test for both. Approved scope otherwise unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): renumber attachment_task_id migration 150 -> 157 after main merge (MUL-4287) Merged latest main; main renumbered its migrations and now occupies 150-156, so 150_attachment_task_id collided with 150_agent_task_coalesced_comments and would fail TestMigrationNumericPrefixesStayUniqueAfterLegacySet. Renamed to the next unique prefix (157). No content change; migrate up applies cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): render agent-produced files as attachment cards, not raw links The chat upload command handed the agent a bare `[name](url)` markdown snippet. Pasted mid-sentence it renders as a plain text link (not a card), and the referenced URL hides the auto-bound standalone attachment — so a file the agent produced could end up showing as nothing. Return the block-level `!file[name](url)` card syntax instead (images keep `` inline), and markdown-escape the filename so names with `[`/`]` don't truncate the label. The prompt and CLI help now state the file auto-attaches below the reply and the snippet is optional, only for placement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): soften message-list scroll fade (32px → 16px) The 32px edge fade washed out full-bleed content (HTML / image previews) at the list edges. Halve the fade distance so it barely grazes previews while still hinting at more content above/below. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): renumber attachment_task_id migration 157 -> 158 main landed 157_agent_task_delivered_comments while this branch was open, colliding on prefix 157 and failing TestMigrationNumericPrefixesStayUniqueAfterLegacySet. Bump this PR's migration to the next free prefix (158). Rename only; the migration body (nullable attachment.task_id + partial index) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): pin attachment upload to the token's task; build index concurrently Two code-review findings on the chat-attachment path (MUL-4287): - Isolation/privacy: POST /api/upload-file only checked the form task_id belonged to the caller's agent, not that it matched the task-scoped token's authoritative X-Task-ID. A run authorized for task A could tag an attachment onto task B (another chat task of the same agent, possibly another user's session), binding it into that reply on completion. Require the form task_id to equal the server-set X-Task-ID; add a same-agent/other-task 403 regression. - Migration: split the task_id lookup index into its own migration (159) built with CREATE INDEX CONCURRENTLY (repo convention) — it cannot share a multi-command file with the ADD COLUMN in 158. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): enforce task-token source on attachment upload; drop transient task_id FK (MUL-4287) Addresses the two remaining Preflight BLOCKERs on PR #5164. Security (file.go): the task_id upload path compared the form task_id to X-Task-ID but did not require X-Actor-Source=task_token. A normal JWT/mul_ PAT leaves that header empty and the middleware does NOT strip a client-forged X-Task-ID; resolveActor's fallback accepts a valid X-Agent-ID+X-Task-ID pair. So a member who learned a task ID could forge both and inject an attachment onto another chat task's assistant reply (cross-session/privacy leak). Now the branch requires X-Actor-Source=task_token first (mirrors chat_history.go's load-bearing boundary), then pins to the middleware-injected X-Task-ID. Tests now go through the real task-token headers and add a forged-JWT-403 regression. Migration (158): task_id is a transient binding handle (written once at upload against an already-validated task, read only during that task's own completion; durable owner is chat_message_id). There is no app-layer path that hard-deletes agent_task_queue rows, and orphan uploads are already reaped by attachment.chat_session_id's ON DELETE CASCADE — so an FK here would only add a cascade dependency the app never relies on plus write overhead on the hot attachment table. Drop the FK; task_id is now a plain UUID column. Added a regression test that an unbound task-tagged upload is reaped on chat_session delete. Index (159, CONCURRENTLY) unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(mobile): align !file card preprocess with web parser + CLI escaped labels (MUL-4287) Howard final-review blocker: mobile's `!file[...]` preprocess didn't keep up with the CLI's file-card output, so agent-produced non-image files rendered nowhere on mobile. - `FILE_LINE_RE` used `[^\]]+` for the label, so the CLI's escaped-bracket output `!file[a\]b.pdf](url)` (cmd_attachment.go escapeMarkdownLabel) never matched — the line stayed literal AND `standaloneAttachments` still hid the fallback card (the URL is in `content`), so the file showed nowhere. - Align the matcher with web's `packages/ui/markdown/file-cards.ts`: label allows backslash-escaped metacharacters (ReDoS-safe class), and the URL is restricted to the same allowlist (site-relative /uploads + /api/attachments/ <UUID>/download, plus absolute http(s)); disallowed schemes stay plain text. - Unescape the label to the real filename, then re-escape only the chars that would break a markdown LINK label (mobile emits `[📎 name](url)`, re-parsed by the renderer — unlike web's HTML data-filename), so a raw `]` never truncates the link text. No dedup change: once the inline `!file` renders, hiding the standalone card is correct. Added focused unit tests covering the escaped-label case, parens/ backslash unescape, the site-relative URL form, and disallowed-scheme rejection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1427e8abd3 | feat(agents): add conversational creation studio (#5296) | ||
|
|
9d453fac1e |
fix(comments): clarify 409 for top-level comment from a comment-triggered task (MUL-4417) (#5292)
* fix(comments): clarify 409 when a comment-triggered task posts a top-level comment (MUL-4417) A comment-triggered task that posted a parentless top-level comment on its own issue got a 409 whose message named the required parent id but never said top-level comments are disallowed. Agents misread it as the issue being locked and deleted good replies trying to reset. Keep the guard (agents must reply under their trigger comment), but make the error self-explanatory and document the constraint in the CLI --parent help. Add handler-level tests pinning the rejected top-level case and the allowed reply-under-trigger case. Refs GH #5266. Co-authored-by: multica-agent <github@multica.ai> * fix(comments): tighten 409 wording and assert the fix hint (MUL-4417) Review nits on #5292: drop the inaccurate "while it is active" phrasing and the redundancy from the 409 message so it matches the actual allow-set (trigger or coalesced comment); collapse the incident narration to one line; and assert the actionable parent_id (--parent) hint in the regression test so the guidance can't be dropped silently. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J (Multica agent) <agent-j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
05d9298582 |
fix(agents): let workspace members view runtime capabilities (MUL-4427) (#5281)
* fix(agents): let workspace members view runtime capabilities (MUL-4427) The Agent capabilities redesign (#5277) reused the runtime local-skills discovery endpoint on Agent detail surfaces, but the endpoint kept the owner-only gate from the original import flow. Viewing an agent bound to someone else's runtime returned 403, which the Skills / MCP tabs rendered as 'try again when the runtime is online' even though the runtime was online. - Discovery (list + poll) now requires workspace membership only; the payload is the deliberately redacted inventory built for this display. - Import (init + poll) stays owner-only: it copies skill file contents off the owner's machine. - The failed notice no longer blames runtime connectivity, and a 403 (new client against an older backend) gets an honest permission message. * test(settings): stub Intl.supportedValuesOf in timezone picker tests The preferences-tab timezone tests drove a ~600-option Base UI Select through userEvent in jsdom; on slow CI runners the clear-preference case exceeded even its extended 20s per-test timeout (PR #5281 frontend job). Stub the IANA enumeration down to the curated COMMON_TIMEZONES fallback — everything the tests pick lives there too — and drop the now-unneeded 20s overrides. File test time drops from ~35s to under 1s. --------- Co-authored-by: Lambda <lambda@multica.ai> |
||
|
|
c377d7fb4f |
feat(labels): add scoped label management (#5279)
* feat(labels): add scoped label management * fix(labels): address review feedback * fix(migrations): use unique label migration prefix |
||
|
|
a14098288b | feat: redesign agent Skills and MCP capabilities (#5277) | ||
|
|
53f05cca5e |
feat(github): fan out PR/check_suite webhooks to all bound workspaces (MUL-4343) (#5218)
* feat(github): fan out PR/check_suite webhooks to all bound workspaces (MUL-4343) One GitHub App installation can be bound to several workspaces (#4855), but pull_request and check_suite webhooks were still routed to a single workspace via resolveWorkspaceForRepo (workspace.repos registry + oldest-binding fallback). Every workspace but one silently received nothing for a shared repo, with no way to opt in. Deliver each repo event to every workspace bound to the installation. Repo scope is whatever GitHub authorized the installation for; we no longer gate on the workspace.repos registry (that list means "code the agent clones", not a webhook subscription). Each workspace independently mirrors the PR, auto-links against its own issue prefix + github toggles, records check suites against its own PR mirror, and gets its own realtime broadcast. - Extract mirrorPullRequestForWorkspace / recordCheckSuiteForWorkspace and loop over all installation bindings instead of resolving one workspace. - Remove the now-dead resolveWorkspaceForRepo / repoIdentityFromURL routing. - Replace the registry-routing tests with PR + check_suite fan-out tests. Co-authored-by: multica-agent <github@multica.ai> * test(github): out-of-order fan-out test + drop dead query + document multi-workspace delivery (MUL-4343) Addresses review feedback on the webhook fan-out change: - Add TestWebhook_CheckSuite_OutOfOrderFansOutToBoundWorkspaces: a check_suite that arrives before the PR must stash a pending row per bound workspace, and each workspace must drain its own row when the PR fans out. - Remove the now-unused ListWorkspacesWithRepos query (its only caller was the deleted resolveWorkspaceForRepo) and regenerate sqlc; fix the stale "picks the target workspace via the repos registry" comment on ListGitHubInstallationsByInstallationID. - Document multi-workspace event delivery in the GitHub integration docs (en + zh), including an explicit self-host upgrade note: delivery is now keyed on the GitHub connection, so a workspace that relied on the code-repository list alone (without connecting GitHub) must connect the installation to keep receiving events. This is an intentional, documented behavior change — the PR description's earlier "single-binding behavior is unchanged" claim was inaccurate and has been corrected. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cb87dd106b |
feat(chat): task-owned direct-chat input batches + explicit no_response outcome (MUL-4351) (#5195)
* feat(chat): task-owned direct-chat input batches + explicit no_response outcome (MUL-4351) Direct (web/mobile) chat no longer uses the last-assistant-row as an implicit input cursor. Each direct send now owns an immutable input batch: - agent_task_queue.chat_input_task_id makes a task the owner of the user messages it must consume; the send path creates the task + user message + attachment bindings + session touch in one transaction, and the daemon is notified only after commit. A claim reads exactly that batch, so a message that arrives mid-run belongs to the next task and is never absorbed. - Auto-retry inherits the root input owner and is queued at a bumped priority, created inside FailTask's transaction so no newer chat task can jump ahead. - CompleteTask writes exactly one assistant outcome inside the completion transaction: a normal message, or a visible no_response outcome (with a non-empty English fallback) when the final output is empty. The write failing rolls the completion back and the handler returns 5xx so the daemon retries; the status CAS keeps it idempotent. chat:done carries message_kind. - Web/desktop/mobile render no_response as a localized 'no text reply' state (keeping the tool timeline), suppress Copy, keep it unread, and keep the session-list preview non-blank. - Legacy/channel tasks (chat_input_task_id NULL) keep the trailing-message selector, so a rolling deploy never replays Slack/Lark history. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): scope no_response to direct tasks; don't cancel task on input read error (MUL-4351) Addresses PR review (Niko): - writeChatCompletionOutcome only writes a no_response row for task-owned direct tasks (chat_input_task_id set). Legacy/channel (Slack/Lark) tasks keep the prior behavior: empty output writes no assistant row, so chat:done carries empty content and the channel outbound silently drops it — the no_response fallback body never reaches an external channel. - The daemon claim distinguishes a genuine zero-input batch from a failed input read: on ListChatInputMessages / ListChatMessages error it returns 5xx and preserves the dispatched task for redelivery instead of cancelling a valid task on a transient DB error. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3c417ea631 |
fix(MUL-4348): authorize + chronologically order per-thread coalesced replies (#5211)
Testing surfaced two problems with the per-thread fan-out:
1. Authorization (blocker): CreateComment rejected any agent comment on the
task's issue whose parent_id != task.TriggerCommentID, so replies to the
OTHER coalesced threads were denied ('parent_id must equal this task's
trigger comment id') and those threads never got a reply. Allow the trigger
comment OR any comment the task coalesced (taskCoversReplyParent: trigger ∪
coalesced_comment_ids); every other parent on the issue is still rejected,
so this stays scoped to the set the run was actually given to answer.
2. Ordering: the agent answered the newest (triggering) comment first. The
fan-out instruction now numbers the targets and explicitly requires posting
OLDEST thread first, the newest/triggering thread last, so replies land in
chronological order. commentReplyThreads already lists oldest-first.
Tests: TestTaskCoversReplyParent (allow-list) and chronological-order
assertions in the cross-thread prompt test.
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
6c3b79db19 |
feat(daemon): bound daemon.log size with rotation (MUL-4330) (#5170)
* feat(daemon): bound daemon.log size with rotation (MUL-4330) The background daemon redirected its stdout/stderr into daemon.log opened O_APPEND and never rotated it, so the file grew without limit until it was too large to open. Every structured log line already flows through slog (including agent subprocess stderr, forwarded via newLogWriter), so the daemon's logger is effectively the sole author of the file's volume. Route the foreground daemon's slog output — both the injected component logger and the package-global slog default — through a size-based rotating writer (lumberjack) that keeps the active daemon.log small (20MB default, 5 gzip-compressed backups, 30d), all env-overridable. Raw crash output (Go runtime panics, pre-logger errors) now goes to a separate daemon.err.log so the child's inherited fds never hold daemon.log open, which would block rotation's rename on Windows. The Desktop app spawns the daemon via this same launcher and its log tail already handles size-shrink, so both CLI and Desktop are covered. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): address log-rotation review — foreground output, Windows handles, bounded err log (MUL-4330) Resolves the blocking review items on the daemon.log rotation change: 1. Windows first-upgrade rotation: a foreground managed daemon now re-points its own stdout/stderr to daemon.err.log at startup (SetStdHandle) before building the rotator, releasing any daemon.log handle an older self-update launcher inherited (Go opens files without FILE_SHARE_DELETE, which would otherwise block rename-on-rotate). No-op on Unix, where an open fd never blocks rename. 2. `daemon logs -f` vs rotation: Unix uses `tail -F` (reopen by name); Windows opens the reader with FILE_SHARE_DELETE so it can't block the rotator's rename, and reopens the file on size-shrink to follow across rotation. 3. Self-update handoff no longer briefly runs two rotators on one file: the old process closes its rotator and moves remaining handoff logs (incl. the slog default) to the crash sink before the successor starts. 4. daemon.err.log is now bounded: it rolls to a single ".1" backup once past 5MB at open time, so a crash loop can't move the growth problem to it. It is also surfaced in the troubleshooting docs. 5. Explicit `--foreground` in a terminal keeps live stdout/stderr logging (a documented debugging path); only detached/background children rotate into daemon.log. Decided by whether stderr is a terminal. Also: rotation env knobs now reject 0/negative (0 means 100MB / keep-all in lumberjack), preventing an accidental unbounded config. Adds unit tests for the err-log rolling and positive-int parsing; Windows/Linux(arm64) cross-builds and `GOOS=windows go vet` pass. Co-authored-by: multica-agent <github@multica.ai> * docs: sync zh/ja/ko troubleshooting with daemon.log rotation + daemon.err.log (MUL-4330) Co-authored-by: multica-agent <github@multica.ai> * test(handler): bump the agent's own runtime version in quick-create parent test (MUL-4330) TestQuickCreateIssueParentTrustBoundary bumped an arbitrary `LIMIT 1` agent_runtime, but the handler version-checks agent.RuntimeID — the runtime bound to the request's agent. In the shared handler test workspace, other tests register additional runtimes, so the two diverge and the agent's real runtime keeps the seed's empty cli_version, tripping the daemon-version gate (422 daemon_version_unsupported) before the parent_issue_id assertions run. Bump the runtime tied to the agent instead, making the setup deterministic. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |