mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-12 20:29:10 +02:00
00415de4630eee17b5fe2fd1fb23f88c19ba9bdf
45 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a2dd80d4f6 |
feat(autopilot): skip dispatch when assignee runtime is offline (MUL-1899) (#2311)
* feat(autopilot): skip dispatch when assignee runtime is offline (MUL-1899) Prevents scheduled autopilots from accumulating doomed tasks against offline / archived / unbound agents. Before this change, a paused laptop or crashed daemon would let a 5-minute-cron autopilot pile up thousands of queued agent_task_queue rows that no runtime would ever drain — this is the dominant source of the 89k stuck-task backlog flagged in MUL-1899. DispatchAutopilot now performs a pre-flight admission check on the assignee agent's runtime status. If the runtime is not 'online' (or the agent is archived / has no runtime bound / has no assignee), the run is recorded as 'skipped' with a failure_reason and no task is enqueued. Skipped runs still emit autopilot:run.done so the UI / activity feed reflect that the trigger fired and was evaluated. Skipped runs are deliberately NOT counted toward the failure-ratio auto-pause: a user who closes their laptop overnight should not have their autopilot paused. Sustained server-side failures keep their existing pause path via the failure monitor. Tests: added an integration test that creates an offline runtime and asserts DispatchAutopilot records a skipped run with no task enqueued. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * feat(scheduler): expire stale queued tasks via TTL sweeper (MUL-1899) Companion to the dispatch-time admission gate added in this PR. The admission gate prevents *new* tasks from being enqueued against an offline runtime, but it does not drain the historical backlog (~89k stuck queued rows observed at MUL-1899 baseline) and does not help when a runtime goes offline *after* a task has already been queued. This adds a passive TTL sweeper: - New SQL query `ExpireStaleQueuedTasks` transitions queued tasks older than the TTL to status='failed' with failure_reason='queued_expired' and a clear error message. - Sweep is capped per tick (`queuedExpireBatchSize`, default 500) via a CTE+LIMIT so that draining a large backlog cannot monopolise the DB on a single tick. At 30s ticks the worst case is 60k rows/hour. - Wired into the existing 30s `runRuntimeSweeper` loop alongside `sweepStaleTasks` and reuses `taskSvc.HandleFailedTasks` so the expired tasks broadcast `task:failed` events, reconcile agent status, and roll back any in-progress issues — same lifecycle as any other failed task. - Default TTL = 2h. Conservatively above any reasonable "queued behind a long-running task" window (default agent timeout is 2h, sweeper runs every 30s) so legitimate work isn't expired. - Integration tests cover the happy path (stale → expired, fresh → left alone, correct status/reason/error) and the per-tick batch cap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix(autopilot): address review blockers from PR #2311 (MUL-1899) GPT-Boy review of the offline-runtime + queued-TTL PR flagged four blockers; this commit addresses them all. 1. Restore the 'skipped' autopilot_run status in the DB constraint. Migration 043 had removed 'skipped' along with the now-defunct concurrency_policy feature, so the new admission gate's INSERT of status='skipped' violated `autopilot_run_status_check` and broke `TestAutopilotDispatchSkipsWhenRuntimeOffline` in CI. New migration 079 re-adds 'skipped' to the CHECK list. The down migration migrates skipped → failed before re-tightening, mirror- ing what 043 did for the original removal. 2. Make `ExpireStaleQueuedTasks` race-safe. The CTE-then-UPDATE pattern could clobber a task that the daemon claimed between victim selection and the outer update. Two guards added: - `FOR UPDATE SKIP LOCKED` in the CTE so we never wait on a row that's currently being claimed (and never block the claim path either). - The outer UPDATE now re-checks `t.status = 'queued'` AND the TTL predicate so even if a row's lock is released after a successful claim, we cannot transition a now-dispatched/ running task to 'failed'. 3. Add a partial index for the queued-TTL sweeper. `idx_agent_task_queue_queued_created_at` on `created_at WHERE status = 'queued'` — keeps the 30s sweep query (status=queued AND created_at < ... ORDER BY created_at LIMIT 500) cheap even when historical terminal rows accumulate (~89k+ at MUL-1899 baseline). The partial predicate keeps the index tiny because only in-flight rows live in 'queued'. 4. Fix the failure-monitor denominator. `SelectAutopilotsExceedingFailureThreshold` had been counting 'skipped' toward total runs, which would have diluted the failure ratio: a 100%-failing autopilot could mask itself behind a wall of admission skips. With 'skipped' restored as a real status, the auto-pause monitor must explicitly exclude it from BOTH numerator and denominator — admission skips are neither a success nor a failure. Verified: `go test ./cmd/server/... ./internal/service/...` passes (including TestAutopilotDispatchSkipsWhenRuntimeOffline, TestExpireStaleQueuedTasks, TestExpireStaleQueuedTasksRespectsBatch Limit). `go build ./... && go vet ./...` clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix(migrations): split queued-task TTL index into concurrent migration Per PR #2311 review: agent_task_queue is a hot table, so building the new partial index with plain CREATE INDEX inside migration 079 would hold ACCESS EXCLUSIVE on the queue and block dispatch during deploy. The migration runner does not allow CONCURRENTLY to share a file with other statements (documented in 068), so split the index into its own single-statement file 080 — matching the existing pattern in 035 / 067 / 074 / 075 / 078. Migration 079 keeps the autopilot_run constraint change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6d9ebb0fdd |
fix(daemon): unblock issues stuck on a poisoned-image agent session (#2314)
* fix(daemon): treat upstream API 400 invalid_request_error as poisoned session A markdown-linked image in an issue description that the agent downloads as a tiny CDN auth-error file and Read's as a PNG poisons the conversation: the LLM API rejects the bad image with 400 invalid_request_error, the session_id is pinned mid-flight, and every follow-up task on the issue (comment-trigger, auto-retry) resumes the same poisoned conversation and hits the same 400 — the issue can no longer be executed even after the description is cleaned up. Mirror the existing fallback-output classifier on the error side: detect "API Error: ... 400 ... invalid_request_error" in the agent error string, persist failure_reason='api_invalid_request', and add it to the GetLastTaskSession exclusion list so the next task starts a fresh session that re-reads the (now-clean) description. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): unblock issues already poisoned by API 400 invalid_request_error The forward-only classifier from the previous commit only tags new failures. Issues like MUL-1918 already have multiple failed-task rows whose failure_reason is the pre-fix default 'agent_error', and GetLastTaskSession falls back to those legacy rows on the next claim — so deploying the classifier alone leaves existing poisoned issues stuck (GPT-Boy review on PR #2314). Two complementary changes: - Migration 079 backfills failure_reason='api_invalid_request' on every pre-existing 'agent_error' row whose error text matches the canonical Anthropic 400 invalid_request_error shape. Keeps observability consistent (multica issue runs / UI now report the right reason). - GetLastTaskSession adds a defensive ILIKE clause on error text. Closes the deploy-window gap where the old binary could write a new 'agent_error' row between the migration running and the new code taking over, and protects against future error-format variants the daemon classifier might miss. Plus regression tests covering the legacy + new coexistence case GPT-Boy flagged, and a guard rail asserting benign 'agent_error' failures (timeouts, tool errors) still resume their session. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0cd50e14eb |
feat(agent-live-card): show queued tasks in issue live banner (MUL-1897) (#2307)
The issue-detail "agent live" banner only showed dispatched/running tasks.
A task that was queued — runtime offline, busy on a prior task, or held
behind a coalesced sibling — left the issue silent until claim, which
reads as "the trigger never landed".
Include 'queued' in `ListActiveTasksByIssue`, then branch the renderer:
queued banners use a non-spinning Clock, "{name} 排队中 / is queued"
copy, "queued for Ns" elapsed anchored on `created_at`, and hide the
transcript button (no execution log yet). Cancel still works because
`CancelAgentTask` already accepts queued.
Client-side re-sort by lifecycle (running → dispatched → queued) so the
sticky slot stays on the most-active task even when a queued sibling
was created more recently.
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
b17f975a17 |
docs(cli): clarify issue rerun semantics (current assignee, fresh session) (#2304)
* docs(cli): clarify `issue rerun` semantics The CLI table described `multica issue rerun <id>` as "Rerun the most recent agent task", which led users to expect it would re-run whichever agent ran last. The actual behavior is to enqueue a fresh task for the issue's **current** agent assignee, regardless of who ran most recently — see `TaskService.RerunIssue` in `server/internal/service/task.go`. Also fix a stale claim in `tasks.mdx`: the "Manual rerun" section described session inheritance as "Yes", but commit |
||
|
|
250ada1fb3 |
chore(db): drop unused agent_task_queue.last_heartbeat_at (#2212)
Drops the unused agent_task_queue.last_heartbeat_at column and removes the hot-path task heartbeat write. |
||
|
|
60b215f44f |
feat(chat): support deleting chat sessions (#2115)
* feat(chat): support deleting chat sessions
Replaces the unreachable archive endpoint with a real hard delete and
exposes it from the chat history panel.
- DELETE /api/chat/sessions/{id} now hard-deletes the session and its
messages (CASCADE), cancels any in-flight tasks before removal so the
daemon doesn't keep running work whose result has nowhere to land,
and broadcasts chat:session_deleted.
- Frontend adds a per-row delete button with a confirmation dialog,
optimistically drops the session from both list caches, and clears the
active session pointer locally + on other tabs via the WS handler.
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): make session delete atomic and keep archived sessions read-only
Address review feedback on #2115.
- DeleteChatSession now runs lock + cancel + delete in a single tx and
only broadcasts events post-commit. The new LockChatSessionForDelete
query takes FOR UPDATE on chat_session, which blocks the FK validation
of any concurrent SendChatMessage trying to enqueue a task for this
session — that insert fails after we commit, so it can no longer
produce an orphaned task whose chat_session_id is nulled by
ON DELETE SET NULL. Cancel failure now aborts the delete instead of
warn-and-continue.
- SendChatMessage refuses non-active sessions again. The archive code
path is gone, but legacy rows with status='archived' may still exist
in the DB; keep the guard until we explicitly migrate them.
- Frontend re-reads allChatSessionsOptions to disable ChatInput on
legacy archived sessions so the UX matches the server-side guard.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
cf47d9b702 | fix: guard session resume by runtime (#1905) | ||
|
|
2dddfaa196 |
feat(daemon): Redis empty-claim fast path for /tasks/claim polling (#1860)
* feat(daemon): Redis empty-claim fast path for /tasks/claim polling Daemons poll /tasks/claim every 30s per runtime; the steady-state warm-empty case currently runs ListPendingTasksByRuntime against Postgres on every poll. This collapses that path: - New ListQueuedClaimCandidatesByRuntime query restricts to status = 'queued' (the old query also returned 'dispatched' rows that can never be reclaimed) and is backed by a partial index keyed on (runtime_id, priority DESC, created_at ASC). - New EmptyClaimCache caches the negative verdict in Redis with a 30s TTL. ClaimTaskForRuntime checks the cache before SELECT and populates it on confirmed-empty results. - notifyTaskAvailable now invalidates the runtime's empty key before kicking the daemon WS, so newly enqueued tasks become claimable immediately rather than waiting out the TTL. - AutopilotService.dispatchRunOnly now goes through TaskService.NotifyTaskEnqueued so run_only tasks get the same invalidate-then-wakeup contract as every other enqueue path. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): close MarkEmpty/Bump race in empty-claim fast path GPT-Boy's review on PR #1860 caught a real concurrency bug. Under the prior implementation it was possible for a slow claim to write an empty verdict AFTER a concurrent enqueue had already invalidated it: T1 claim: SELECT -> empty T2 enqueue: INSERT row, DEL empty key (no-op, key not set yet), wakeup T1 claim: SET empty (writes a stale "empty" verdict) T3 wakeup: IsEmpty -> hit -> returns null The just-queued task would then sit idle until the empty key's TTL expired (up to 30s). Replace the DEL-based invalidation with a per-runtime version counter: - CurrentVersion(rt) is a Redis INCR counter at mul:claim:runtime:version:<rt> with a 24h sliding TTL. - Claim samples version BEFORE the SELECT and passes it to MarkEmpty, which stores the verdict's value as the observed-version string. - IsEmpty MGETs both keys and trusts the verdict only when the empty-key value equals the current version. - Enqueue Bumps the version (INCR + EXPIRE) before the wakeup, causing any verdict written under a prior version to be rejected on the next read. Also bound every Redis call from this cache with a 250ms timeout — notifyTaskAvailable uses a background context so a wedged Redis must not block enqueue. Tests against a real Redis (REDIS_TEST_URL) cover: - MarkEmpty + IsEmpty under matching version returns hit - Bump invalidates a prior empty verdict (race-fix pin) - A MarkEmpty written under a stale pre-Bump version is rejected - TTL clamping, per-runtime isolation, nil-cache safety - notifyTaskAvailable Bumps before the wakeup fires Co-authored-by: multica-agent <github@multica.ai> * chore(daemon): renumber claim-candidate index migration to 067 Slot 064 was taken on main by 064_notification_preference. The migration runner tracks per-version in schema_migrations and would silently skip the second 064_*, leaving the index uncreated. Rename to 067 (next free slot). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b1345685a3 |
fix(task): rerun starts a fresh session, skip poisoned resume (#1928)
* fix(task): rerun starts a fresh session, skip poisoned resume
When a task ended in a known agent fallback ("I reached the iteration
limit and couldn't generate a summary.", "Put your final update inside
the content string. Keep it concise.") the (agent_id, issue_id) resume
lookup would still pick that session, so a manual rerun inherited the
poisoned state and reproduced the same bad output.
Two complementary guards:
1. Daemon classifies poisoned terminal output and routes it through the
blocked path with failure_reason set ('iteration_limit' /
'agent_fallback_message'). GetLastTaskSession excludes failed tasks
with those reasons, so even comment-triggered tasks no longer resume
them. Tasks that failed mid-flight (timeout, runtime_recovery, etc.)
are still resumable, preserving MUL-1128's auto-retry contract.
2. Manual rerun marks the new task force_fresh_session=true. The daemon
claim handler skips the resume lookup entirely when the flag is set,
capturing the user-intent signal that "the prior output was bad" even
when poisoned classification misses a future fallback wording.
Auto-retry of orphaned mid-flight failures (MaybeRetryFailedTask →
CreateRetryTask) does not take this path, so it keeps resuming.
Tests: classifyPoisonedOutput unit test; integration tests assert the
SQL filter excludes poisoned classifiers, RerunIssue flips the flag,
and the normal enqueue path leaves it false.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): cap poisoned-output matcher to short trimmed text
GPT-Boy review on MUL-1630: the previous strings.Contains match would
classify any output that quoted the marker substring — including a
review/analysis that simply discussed the marker itself. Real fallback
messages are short single-sentence affairs, so cap the candidate at
~one paragraph and trim whitespace before matching. Adds regression
tests covering a long quoting review and a marker buried in a long
real conclusion; both must stay classified as completed.
Co-authored-by: multica-agent <github@multica.ai>
* fix(migrations): rename 065 force_fresh_session → 066 to clear collision
main introduced 065_project_resources after this branch was cut, so
both files shared the 065_ prefix. The readiness check
(server/cmd/server/health.go → migrations.LatestVersion) takes the
last entry by lexical order, which is 065_project_resources, leaving
this branch's 065_force_fresh_session unguarded — a deploy that
applied project_resources but not force_fresh_session would still
report ready, and the next enqueue / rerun / claim would crash on
"column force_fresh_session does not exist".
Renaming to 066_force_fresh_session puts it strictly after
project_resources so readiness blocks until it's applied.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
9baa72cc68 |
fix: polish quick-create UX (kind labeling, dark toast, placeholder) (#1831)
* fix: polish quick-create UX (kind labeling, dark toast, placeholder)
Three small fixes shaken out from using the agent-create flow:
- AgentTaskResponse now carries a `kind` discriminator
("comment" | "autopilot" | "chat" | "quick_create" | "direct"), computed
from the existing FK shape with no extra DB access. The Activity row
uses it to label quick-create tasks as "Creating issue" instead of
falling through to the generic "Untracked" — once the agent finishes
and the new issue is linked, the row transitions to the normal
identifier+title display.
- Sonner Toaster reads `resolvedTheme` instead of `theme`, so toasts
follow the actual dark/light state. Forwarding "system" let sonner
pick its own answer from `prefers-color-scheme`, which in the Electron
renderer can disagree with next-themes' `html.dark` class — the toast
rendered light on a dark UI.
- Agent-create placeholder rephrased to a more conversational example
with a project reference: "let Bohan fix the inbox loading slowness
in the Web project". Drops the priority hint (priority isn't widely
used) and matches how people actually instruct the agent.
* fix(quick-create): link new issue back to task on completion
Addresses the review on PR #1831: completed quick-create tasks were
left with issue_id=NULL forever, so the activity row stayed on
"Creating issue" instead of transitioning to the normal MUL-XXX +
title rendering once the agent finished.
- Server: notifyQuickCreateCompleted now writes the resolved issue id
back to agent_task_queue.issue_id via a new LinkTaskToIssue query
(guarded by `issue_id IS NULL` so it only ever fills the unset
quick-create case). Best-effort: a write failure logs but doesn't
block the inbox notification.
- Frontend: defensive wording fallback — kind=quick_create rows in
terminal status (completed/failed/cancelled) now render as
"Quick create" instead of the active "Creating issue" label,
covering rows whose link write failed or whose agent never
produced an issue at all.
|
||
|
|
f745a3bbbe |
feat(agent): presence v3 + execution log + trigger summary (#1823)
* refactor(views): migrate agent/runtime/skill lists to TanStack DataTable
Replace the per-page CSS Grid + minmax(min, fr) + sticky-first-col + truncate
implementation with a TanStack Table backend rendered through a Dice UI-style
DataTable shell. Column widths are now px-based via column.size, so cells
no longer shrink or auto-truncate as the viewport narrows; when the sum of
columns exceeds the viewport, the container scrolls horizontally instead.
- Add @tanstack/react-table to the catalog (8.21.3) and wire it into
packages/ui (dep) and packages/views (peerDep).
- packages/ui: new DataTable + DataTableColumnHeader + lib/data-table.ts
(getColumnPinningStyle), adapted from Dice UI's registry. The shell
renders <table> directly (skipping shadcn's <Table> wrapper) so its own
outer overflow controls both axes — no nested overflow conflicts.
- packages/views: each list now declares ColumnDef[] with explicit
cell renderers. Row click navigates to detail via onRowClick (instead of
wrapping <tr> in <a>, which is invalid HTML); kebab dropdowns
stopPropagation so they don't trigger the row navigation.
- Drop the previous AGENT_LIST_GRID / GRID_WITH_OWNER / ROW_GRID
templates and the sticky-first-col / subgrid mechanics that came with
them. agent-list-item.tsx is removed; runtime-list.tsx and
skills-page.tsx are trimmed to thin wrappers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agent): cap description at 255 chars (db + api + ui)
Symmetric enforcement across DB, server, and UI:
- Migration 060: pre-flight truncate of any oversize rows, then ADD
CONSTRAINT NOT VALID + VALIDATE CONSTRAINT so the new check doesn't
block writes during validation.
- Server handler validates utf8.RuneCountInString on Create/Update and
rejects over-limit input with 400.
- Front-end gets AGENT_DESCRIPTION_MAX_LENGTH in core/agents/constants
(single source of truth shared by the create dialog + edit modal +
test suite) and a CharCounter component that warns at 90% and errors
past the cap.
- Description editor moves from a 288px popover to a roomy modal.
Editor body is mounted only while the dialog is open, so the local
draft state is locked in at mount time and never reset by an external
WS update — the React-recommended replacement for the
useEffect(reset, [value]) anti-pattern.
Counted in code points everywhere (rune count / spread length /
char_length) so multibyte input agrees across all three layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(views): data-table polish across runtime + skill lists
Builds on the DataTable migration in
|
||
|
|
2d9c153695 |
feat: quick-create issue (async agent + inbox completion) (#1786)
* feat(server): add quick-create issue async task path Adds POST /api/issues/quick-create which validates the picked agent's reachability up front (not archived, has runtime, runtime online) then queues an issue-less agent task whose context JSONB carries the user's natural-language prompt + requester + workspace. Daemon claim resolves the workspace from the context, and the prompt builder switches to a quick-create template instructing the agent to translate the prompt into a single multica issue create call. Task completion writes a success inbox item to the requester pointing at the newly-created issue (located by querying the agent's most recent issue in the workspace since task start, so we don't depend on agent stdout shape). Failures write an action_required inbox item carrying the original prompt + agent id so the frontend can offer "Edit as advanced form" without losing input. * feat(views): quick-create issue modal + inbox failure CTA Adds a streamlined create-issue UI bound to the c shortcut: pick an agent, type one line, submit. The modal closes immediately and the agent translates the prompt into a multica issue create call in the background. Shift+c keeps the legacy advanced form for users who want every field. The "Advanced" button inside the new modal seeds the shared issue-draft store with the prompt + picked agent so switching mid-flow doesn't lose input. Last-used agent persists per (user, workspace) via a workspace-aware zustand store so frequent users skip the picker on every open. Inbox renders quick_create_done items with a status pin to the new issue and quick_create_failed items with an "Edit as advanced form" CTA that re-seeds the legacy modal with the original prompt. ApiError now carries the parsed JSON body so the modal can branch on the structured agent_unavailable code without parsing the error message. * fix(quick-create): execenv injection, claim race, private-agent permission Addresses GPT-Boy review on #1786: 1. execenv was rendering the assignment-task issue_context.md / runtime workflow even for quick-create, telling the agent to call `multica issue get/status/comment add` against an empty IssueID. Adds QuickCreatePrompt to TaskContextForEnv, plus a quick-create branch in renderIssueContext + the runtime_config workflow that instructs the agent to run a single `multica issue create` and exit, with explicit "do NOT call issue get/status/comment add" guards. 2. ClaimAgentTask serialized only on issue_id / chat_session_id, so concurrent quick-creates on the same agent (both NULL on those columns) ran in parallel — making the success-inbox lookup race over "most recent issue by this agent". Adds a third OR clause that treats "all four FKs NULL" as a serialization key for the same agent, so quick-create tasks on a given agent run one at a time. 3. QuickCreateIssue handler bypassed the private-agent ownership rule that validateAssigneePair enforces elsewhere — a user could POST a private agent_id they didn't own and trigger it. Now routes the picked agent through validateAssigneePair before the runtime liveness check. 4. Clarifies the quick-create-store namespacing comment to match the actual workspace-aware StateStorage convention used by the other issue stores (per-user is browser-profile-local). * fix(quick-create): branch Output section + deterministic origin lookup Addresses GPT-Boy's second-pass review on #1786: 1. The runtime_config.go Output section forced "Final results MUST be delivered via multica issue comment add" for every non-autopilot task — quick-create still got this conflicting instruction even though there's no issue to comment on. Switched the Output block to a three-way switch so quick-create gets a tailored "stdout is captured automatically; do NOT call comment add" branch matching the autopilot variant. 2. Completion lookup was "most recent issue created by this agent since task.started_at", which races against concurrent issue creates by the same agent (assignment task running alongside quick-create when max_concurrent_tasks > 1). Replaced with a deterministic origin link: - Migration 060 extends issue.origin_type CHECK to allow 'quick_create'. - Daemon sets MULTICA_QUICK_CREATE_TASK_ID env var when running a quick-create task. - multica issue create CLI reads the env var and stamps the new issue with origin_type=quick_create + origin_id=<task_id>. - Server CreateIssue handler accepts (origin_type, origin_id) from trusted callers (only "quick_create" is allowed; the pair is rejected unless both fields are provided together). - notifyQuickCreateCompleted now calls GetIssueByOrigin keyed on (workspace_id, "quick_create", task.ID) — no more time-window racing against parallel agent activity. The old GetRecentIssueByCreatorSince query is removed. |
||
|
|
21e3cfaa01 |
Agent runtime status redesign: split presence into availability + last-task (#1794)
* feat(agent-status): add workspace live-tasks endpoint and TaskFailureReason type Lays the API + type contract for the front-end agent presence cache: - New `GET /api/active-tasks` returns active (queued/dispatched/running) tasks plus failed tasks within the last 2 minutes for the current workspace. The 2-minute window powers a UI-side auto-clearing "Failed" agent state without back-end pollers. - `agent_task_queue` has no workspace_id column, so the query JOINs agent; `SELECT atq.*` keeps `failure_reason` (migration 055) on the wire. - Adds `TaskFailureReason` to `AgentTask` so the UI can map the 5 backend classifiers (agent_error / timeout / runtime_offline / runtime_recovery / manual) to copy without parsing free-text errors. - New `api.getActiveTasksForWorkspace()` client method; workspace is resolved server-side from the X-Workspace-Slug header (no path param, matching /api/agents and /api/runtimes conventions). Includes the joint engineering plan and designer brief that scope the broader Agent / Runtime status redesign — Phase 0 is this contract plus the front-end derivation layer landing in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(agent-status): derive presence/health states with WS sync and desktop IPC bridge Adds the front-end derivation layer that turns raw server data into the user-facing 5-state agent / 4-state runtime enums. UI files are deliberately untouched in this commit — derivation lives behind hooks (useAgentPresence, useRuntimeHealth) that any component can call with zero additional network traffic. Architecture: - Derivation is pure functions in packages/core/{agents,runtimes}; the back-end stays free of UI translation. Agents algorithm: runtime offline > recent failed (2-min window) > running > queued > available. Runtimes algorithm: status + last_seen_at -> online / recently_lost / offline / about_to_gc. - A single workspace-wide active-tasks query backs all per-agent presence reads, eliminating N+1 across hover cards, list rows, and pickers. 30-second tick re-renders the hooks so the failed window expires even when no underlying data changes. - WS task lifecycle events (dispatch / completed / failed / cancelled) invalidate active-tasks via the prefix dispatcher. completed/failed were removed from specificEvents so they go through both the prefix invalidate and the existing chat ws.on() handlers. Reconnect refetch picks up active-tasks too. - Desktop bridges window.daemonAPI.onStatusChange directly into the runtimes cache via setQueryData, giving the local daemon sub-second feedback (vs. 75s server sweep). Bridge is wsId-bound so workspace switches automatically rebind the subscription; daemon_id matching covers the same-daemon-multiple-providers case. 24 derivation unit tests cover all branches plus null/empty/boundary inputs (FAILED_WINDOW_MS edges, null last_seen_at, missing completed_at). Full core suite: 112 tests passing. Typecheck green across all 8 workspace packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(agent-status): redesign agent runtime status as two orthogonal dimensions Splits the conflated 5-state agent presence into two independent axes: - AgentAvailability (3-state): online / unstable / offline — drives the dot indicator everywhere a dot appears. Pure runtime reachability; never sticky-red because of a past task outcome. - LastTaskState (5-state): running / completed / failed / cancelled / idle — surfaced as text + icon on focused surfaces (hover card, agent detail page, agents list, runtime detail). Never colours the dot. Major changes: * Domain layer: AgentPresence union → AgentAvailability + LastTaskState. derive-presence split into deriveAgentAvailability + deriveLastTaskState + deriveAgentPresenceDetail orchestrator. Tests reorganised into three groups (availability invariants, last-task invariants, composition). * Visual config: presenceConfig (5 entries) → availabilityConfig (3) + taskStateConfig (5). availabilityOrder + lastTaskOrder for filter chips. * Workspace-level presence prefetch: new useWorkspacePresencePrefetch hook + WorkspacePresencePrefetch mount component, wired into DashboardLayout (web) and WorkspaceRouteLayout (desktop). Hover cards render synchronously with no skeleton flash on first hover. * ActorAvatar hover: flipped default — disableHoverCard removed, enableHoverCard added (default false). Opt-in at ~14 decision-moment surfaces; pickers / decoration sub-chips stay plain. Status dot decoupled (showStatusDot prop) so picker rows can show presence without nesting popovers. * Hover cards: AgentProfileCard simplified — availability dot only, Detail link top-right (logs live on the detail page). New MemberProfileCard mirrors the structure: name + role + email + top-2 owned agents (sorted by 30d run count) with click-through to agent detail. * Agents list: split Status into two columns — availability (3-color dot + label) and Last run (task icon + label, optional running counts). Two independent filter chip groups (Status + Last run); combination acts as intersection ("online + failed" finds broken- but-alive agents). * Other UI surfaces (issue list/board/detail, comments, autopilots, projects, runtimes, mention autocomplete, subscribers picker) updated to the new dot semantics; status dot now strictly 3-color. Server changes accompany the client redesign — workspace-wide agent-task-snapshot endpoint, runtime usage queries, etc. — to feed the derive layer with the data it needs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(agent-detail): drop last-task chip from detail header + inspector The Recent work section on the agent detail page already shows the same data (with task titles, timestamps, error context) — surfacing "Completed" / "Failed" / etc. up in the header was redundant chrome. Detail surfaces now show only the 3-state availability dot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tables): handle narrow viewports across agents / skills / runtimes Three table layouts were squeezing content into adjacent cells at intermediate widths. Each fix is small and targeted: * runtime-list: the Runtime cell's base name had `shrink-0`, so it refused to truncate when its grid column was narrowed under width pressure — the name visually overflowed into the Health column ("ClaudeOnline" etc). Removed shrink-0, added truncate. The Health column was also a fixed 9.5rem reservation for the worst-case "Recently lost · 2m 14s ago" copy; switched to minmax(0,1fr) so it competes fairly with Runtime. * skills-page: had a single grid template with no responsive breakpoints — all 6 columns were rendered at any width and got visually jammed below md. Added a <md template that drops Source + Updated; the row markup hides those cells via `hidden md:block` / `md:contents`. * agent-list-item: the new Last run column was reserved at minmax(8rem, max-content); on narrow md viewports the 8rem floor pushed the row past available width. Changed to minmax(0,max-content) so the cell shrinks under pressure (its content already truncates). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(agent-card): hover-only Detail + add Runtime row + breathing room Three small polish tweaks to the agent hover card: - Detail link gets `mr-1` + fades in only on card hover (group-hover). It was visually flush against the popover edge and competing for attention; now it stays out of the way during a quick glance and surfaces only when the user is dwelling on the card. - Runtime row is back, in the meta block (cloud/local icon + runtime name). The earlier removal was over-aggressive — knowing where an agent runs is part of "who is this agent". The wifi badge stays dropped because the availability dot in the header already conveys reachability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(runtime): wifi-style health icon (4-state) for runtime list + agent card Replaces the 6px coloured dot with a wifi-shape icon that carries both state (Wifi vs WifiOff) and severity (success/warning/muted/destructive). Mapping: - online → Wifi (success) - recently_lost → WifiHigh (warning) — transient hiccup, fewer bars - offline → WifiOff (muted) — long unreachable - about_to_gc → WifiOff (destructive) — sweeper coming soon Used in two places: - Runtime list: replaces HealthDot in the dedicated leading-icon column. Bumped the column from 0.5rem (dot-sized) to 0.875rem (icon-sized). - Agent profile card RuntimeRow: derives runtime health from runtime + clock (matching the 4-state semantics) and renders HealthIcon next to the runtime name. Cloud runtimes always read as online. The duplicate signal with the header availability dot is intentional — it confirms WHICH runtime is the one currently in the dot's state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b77acdf642 |
fix(comments): cancel triggered tasks when comment is deleted (#1747)
When a user deletes a comment that triggered an agent task, the agent would still run with the now-deleted content baked into its prompt (fetched at task claim time) — manifesting as "the agent still sees the deleted comment". The FK ON DELETE SET NULL only nullified trigger_comment_id; the queued task itself was never cancelled. DeleteComment now cancels any queued/dispatched/running task whose trigger is the deleted comment, before the comment row is removed. |
||
|
|
ba2f19d631 |
fix: refresh agent status from active tasks (#1733)
Co-authored-by: Eve <eve@multica.ai> |
||
|
|
3c3e3bd330 |
fix(task): reconcile agent status when cancelling tasks by issue (#1587) (#1648)
CancelTasksForIssue silently dropped the list of affected tasks, so whenever an issue transitioned to "cancelled" or "done" while a task was still active (6 call sites in issue.go), the underlying agent was left stuck at status="working" indefinitely and required a manual `multica agent update <id> --status idle` to self-correct. This matches the symptom reported in #1587: task rows move to "cancelled" via a non-user-initiated path, agent status never reconciles. Change CancelAgentTasksByIssue from :exec to :many (also tack on completed_at = now() for consistency with CancelAgentTasksByIssueAndAgent), then update CancelTasksForIssue to iterate the returned rows and call ReconcileAgentStatus + broadcast task:cancelled per affected task — mirroring the pattern already used by CancelTask and RerunIssue. No test added; the change is small and mirrors well-covered paths. Happy to add a mock-backed test in a follow-up if reviewers prefer. Refs #1587 Refs #1149 |
||
|
|
0b1333fb00 |
feat(server): orphan-task recovery + auto-retry + manual rerun (MUL-1128) (#1476)
* feat(server): orphan-task recovery + auto-retry + manual rerun (MUL-1128)
When the daemon process crashed mid-task the issue was stuck at
in_progress for up to 2.5h: the in-flight task timeout was the only
mechanism that ever moved the row, and the runtime heartbeat sweeper
only fires after the runtime stays offline for 45s — a quick restart
beats both windows.
This change implements the A+B plan from the issue thread:
A. lifecycle hygiene
- migration 055 adds attempt / max_attempts / parent_task_id /
failure_reason / last_heartbeat_at to agent_task_queue
- new daemon-auth endpoint POST /runtimes/{id}/recover-orphans:
daemon calls it on every register so the server fails any
dispatched/running tasks the previous process left behind
- new daemon-auth endpoint POST /tasks/{id}/session: persists the
agent's session_id + work_dir mid-flight so a crash doesn't
lose the resume pointer (claude+codex emit MessageStatus with
SessionID; daemon forwards on the first one it sees)
- FailAgentTask / FailStaleTasks / FailTasksForOfflineRuntimes
now set failure_reason ('agent_error' / 'timeout' /
'runtime_offline')
B. auto-retry with resume context
- TaskService.MaybeRetryFailedTask spawns a fresh queued attempt
carrying parent's session_id/work_dir when the failure reason
is infrastructure-shaped (timeout, runtime_offline,
runtime_recovery) and attempt < max_attempts; skips autopilot
- wired into the runtime sweeper paths and TaskService.FailTask
so the user transparently sees a new in_progress run instead of
a stuck row
- new user-auth POST /api/issues/{id}/rerun + multica issue rerun
CLI for the manual escape hatch
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(server): address PR review for orphan-task recovery (MUL-1128)
Three review-must-fix items on top of the A+B implementation:
1. recover-orphans now funnels through TaskService.HandleFailedTasks,
the same shared post-failure pipeline used by the runtime sweeper.
This guarantees task:failed events are emitted, agent status is
reconciled, and issues stuck in_progress with no remaining active
task are reset to todo even when no auto-retry is created
(max_attempts exhausted, autopilot, non-retryable reason).
2. RerunIssue now uses CancelAgentTasksByIssueAndAgent, scoped to the
issue's current assignee. The previous implementation called
CancelAgentTasksByIssue, which would collateral-cancel parallel
@-mention agents on the same issue.
3. GetLastTaskSession now considers both completed and failed tasks
(mirroring GetLastChatTaskSession), ordering by the most recent
timestamp. With UpdateAgentTaskSession pinning session_id/work_dir
mid-flight, an auto-retry or manual rerun of a daemon-crash failure
now actually resumes the prior conversation context instead of
starting fresh — matching the stated B-branch behaviour.
go build / go vet pass; the existing service and agent test suites pass.
runtime_sweeper / handler integration tests require a local DB with the
055 migration (and the pre-existing 050 first_executed_at column).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
b291db11c2 |
feat(agents): add per-agent model field with provider-aware dropdown (#1399)
Adds a first-class `model` field on agents so users can pick the LLM model from the create / settings UI instead of editing `custom_env` / `custom_args`. Each provider's dropdown is populated from the live CLI when possible (`opencode models`, `pi --list-models`, `openclaw agents list --json`, `cursor-agent --list-models`, hermes ACP `session/new` → `SessionModelState`), with a static catalog for providers that don't enumerate.
Daemon resolves the runtime model as `agent.model → MULTICA_<PROVIDER>_MODEL → ""` — empty passes through so each backend's CLI picks its own default, avoiding static-guess drift.
Per-provider honouring:
- Claude / Codex / OpenCode / Cursor / Gemini / Pi / Copilot — CLI `--model` / thread payload.
- OpenClaw — `opts.Model` is mapped to `--agent <name>` (the CLI rejects `--model`).
- Hermes — `session/set_model` ACP RPC; stderr is sniffed for provider-level errors so HTTP 4xx from the configured LLM surfaces instead of "empty output"; explicit-model failures mark the task `failed`.
Supporting changes: migration 050 adds `agent.model`; daemon ↔ server heartbeat piggyback carries a model-discovery request; new REST endpoints under `/api/runtimes/{id}/models`; `multica agent create --model` / `update --model`; shared `ModelDropdown` in `packages/views/agents` (searchable, creatable, provider-grouped, default-badge, runtime-supported gate).
|
||
|
|
5fa1da448f |
fix(chat): preserve chat session resume pointer across failures (#1360)
* fix(chat): preserve chat session resume pointer across failures The chat 'forgets earlier messages' bug came from PriorSessionID being silently lost in several edge cases: - UpdateChatSessionSession unconditionally overwrote chat_session.session_id, so any task that completed without a session_id (early agent crash, missing result) wiped the resume pointer to NULL. - CompleteAgentTask + UpdateChatSessionSession ran in separate calls. A follow-up chat message claimed in between resumed against a stale (or NULL) session and started over. - FailAgentTask never wrote session_id back, so a task that established a real session before failing lost its resume pointer. - ClaimTaskByRuntime only trusted chat_session.session_id and never fell back to the existing GetLastChatTaskSession query, so a single bad turn could permanently drop the conversation memory. This change: - Use COALESCE in UpdateChatSessionSession so empty inputs preserve the existing pointer; surface DB errors instead of swallowing them. - Run CompleteAgentTask/FailAgentTask + UpdateChatSessionSession inside the same transaction (TaskService now takes a TxStarter). - Extend FailAgentTask + the daemon FailTask path (client, handler, service) to forward session_id/work_dir, so failed/blocked tasks that built a real session still record it. - Fall back to GetLastChatTaskSession in ClaimTaskByRuntime when the chat_session pointer is missing, and include failed tasks in that lookup so a single failure can't lose the conversation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(daemon): forward session_id/work_dir on blocked + timeout paths runTask previously dropped result.SessionID and env.WorkDir on the non-completed return paths: - timeout returned a naked error, so handleTask called FailTask with empty session info and the chat resume pointer was either left stale or eventually overwritten with NULL. - blocked / failed (default branch) returned a TaskResult without SessionID / WorkDir, so even though FailTask now COALESCEs into chat_session, there was no value to write through. - the empty-output completion path was the same: it raised an error even when a real session_id had been built. All three paths now return a TaskResult that carries the SessionID / WorkDir the backend produced. Combined with the COALESCE-based update in UpdateChatSessionSession and the FailTask plumbing introduced in PR #1360, the next chat turn can always resume from the latest agent session — even when the previous turn timed out, was rate-limited, or returned an empty completion — instead of starting over with no memory of the conversation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(copilot): capture session id from session.start as fallback The Copilot backend only read sessionId from the synthetic 'result' event, ignoring the one already present on session.start. When the CLI was killed before result arrived (timeout, cancel, crash, or a session.error mid-turn), the daemon reported SessionID="" and the chat-session resume pointer could not advance — causing the chat to silently drop conversation memory on the next turn. Capture session.start.sessionId into state up front, and only let 'result' overwrite it when it actually carries one. result still wins when present (it is the authoritative end-of-turn record). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(copilot): parse premiumRequests as float to preserve session id Copilot CLI v1.0.32 serializes premiumRequests as a float (e.g. 7.5), not an integer. Our copilotResultUsage struct typed it as int, which made the entire 'result' line fail json.Unmarshal — silently dropping sessionId on every turn. This was the real cause of chat memory loss: the daemon reported SessionID="" to the server, chat_session.session_id stayed NULL, and the next chat turn never received --resume <id>, so each turn started a fresh Copilot session with no prior context. Add a regression test using the real JSON line from CLI v1.0.32 that asserts sessionId is preserved when premiumRequests is fractional. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Eve <eve@multica.ai> Co-authored-by: yushen <ldnvnbl@gmail.com> |
||
|
|
63800f05ff |
fix(agent): add per-agent mcp_config field to restore MCP access (#1168)
* fix(agent): add per-agent mcp_config field to restore MCP access Closes #1111 The --strict-mcp-config flag was added defensively in #592 to prevent Claude agents from inheriting MCP state from the outer Claude Code session. It was meant to be paired with --mcp-config <path> to inject a controlled set of MCPs, but that path was never implemented, which silently stripped all user-scope MCPs from spawned agents. This PR completes the original design by: - Adding a nullable mcp_config jsonb column to the agents table - Wiring mcp_config through AgentResponse, Create/Update requests - Piping it into ExecOptions.McpConfig in the daemon - Serializing to a temp file and passing --mcp-config <path> in buildClaudeArgs - Blocklisting --mcp-config in claudeBlockedArgs to prevent override via custom_args Does not touch Codex provider (tracked separately in #674). Does not implement Multica MCP auto-injection (out of scope). * fix: disambiguate JSON null vs absent for mcp_config |
||
|
|
ce447c7f06 |
feat(agent): add custom CLI arguments support (#986)
* feat(agent): add custom CLI arguments support Allow users to configure custom CLI arguments per agent that get appended to the agent subprocess command at launch time. This enables use cases like specifying different models (--model o3), max turns, or other provider-specific flags without needing separate runtimes. Changes: - Add custom_args JSONB column to agent table (migration 041) - Update API handler to accept/return custom_args in create/update - Pass custom_args through claim endpoint to daemon - Append custom_args to CLI commands for all agent backends - Add ExecOptions.CustomArgs field in agent package - Add Custom Args tab in agent detail UI - Add --custom-args flag to CLI agent create/update commands Closes MUL-802 * fix(agent): filter protocol-critical flags from custom_args Add per-backend filtering of custom_args to prevent users from accidentally overriding flags that the daemon hardcodes for its communication protocol (e.g. --output-format, --input-format, --permission-mode for Claude). This follows the same pattern as custom_env's isBlockedEnvKey: we only block the small, stable set of flags that would break the daemon↔agent protocol — not every possible dangerous flag. Workspace members are trusted for everything else. Each backend defines its own blocked set: - Claude: -p, --output-format, --input-format, --permission-mode - Gemini: -p, --yolo, -o - Codex: --listen - OpenCode: --format - OpenClaw: --local, --json, --session-id, --message - Hermes: none (ACP is positional) Includes unit tests for the filtering logic. * fix(agent): address code review nits for custom_args - Replace module-level `nextArgId` counter with `crypto.randomUUID()` in custom-args-tab.tsx to avoid SSR ID conflicts - Add unit tests for custom args passthrough and blocked-arg filtering in both Claude and Gemini arg builders |
||
|
|
d88fe2608e |
feat(autopilot): scheduled/triggered automations for AI agents (#1028)
* feat(autopilot): add scheduled/triggered automation for AI agents Introduce the Autopilot feature — recurring automations that assign work to AI agents on a schedule or manual trigger. Supports two execution modes: create_issue (creates an issue for the agent to work on) and run_only (directly enqueues an agent task without issue pollution). Backend: migration (3 tables + 2 columns), sqlc queries, AutopilotService with concurrency policies (skip/queue/replace), HTTP CRUD + trigger endpoints, background cron scheduler (30s tick), event listeners for issue→run and task→run status sync. Frontend: types, API client methods, TanStack Query hooks with optimistic mutations, realtime cache invalidation, list page with create dialog, detail page with trigger management and run history, sidebar nav + routes for both web and desktop apps. * feat(autopilot): improve UX — trigger config, edit dialog, template gallery - Replace raw cron input with friendly frequency tabs (Hourly/Daily/Weekdays/Weekly/Custom), time picker, and timezone dropdown defaulting to user's local timezone - Fix Select components showing UUIDs instead of names (Base UI render function pattern) - Add Edit button on detail page opening a unified edit dialog - Remove project/concurrency/issue-title-template from create/edit (simplify for users) - Add trigger configuration inline during autopilot creation - Add template gallery on empty state (6 step-by-step workflow templates) - Rename "Description" to "Prompt" throughout UI - Inject autopilot run timestamp into issue description for agent date awareness - Treat issue status "in_review" as run completion (fixes skip on next trigger) - Make migration idempotent with IF NOT EXISTS clauses |
||
|
|
4165401d16 |
feat(agent): support custom environment variables for router/proxy mode
Add per-agent custom_env configuration that gets injected into the agent subprocess at launch time. This enables users to configure custom API endpoints (ANTHROPIC_BASE_URL), API keys (ANTHROPIC_API_KEY), and cloud provider modes (CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX) without requiring code changes. Changes: - Migration 040: add custom_env JSONB column to agent table - Backend: custom_env in agent CRUD API + claim endpoint - Daemon: merge custom_env into subprocess environment variables - Frontend: env var editor in agent settings (key-value pairs with visibility toggle for sensitive values) Closes #816 Related: #807, #809 |
||
|
|
50f9e673e8 |
feat(chat): add agent chat feature (full stack)
Implement the Master Agent chat feature allowing users to chat with agents directly from a floating window, separate from the issue-based workflow. Backend: - New chat_session and chat_message tables (migration 033) - Make issue_id nullable on agent_task_queue for chat tasks - REST API: create/list/get/archive sessions, send/list messages - EnqueueChatTask in TaskService with session_id persistence - WS events: chat:message, chat:done - Daemon: chat task type with separate prompt builder - ClaimTaskByRuntime populates chat context (session, message, repos) Frontend: - ChatSession/ChatMessage types + API client methods - core/chat: TanStack Query options, mutations with optimistic updates, WS updaters - features/chat: Zustand store, ChatFab (floating button), ChatWindow with real-time streaming via task:message events - Mounted in dashboard layout (bottom-right corner) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
98af9f442c |
Merge pull request #471 from multica-ai/agent/j/959392dd
feat: support multiple agents running on same issue |
||
|
|
7c79611309 |
refactor: remove agent triggers config field (#469)
* refactor: remove agent triggers config field Remove the triggers field from agent configuration. The on_assign, on_comment, and on_mention behaviors are now always enabled (hardcoded), as decided in the Agentflow design discussion (MUL-372). Changes: - Database: migration 032 drops triggers column from agent table - Backend: remove triggers from create/update agent APIs and response - Backend: simplify trigger-checking logic to always-enabled - Frontend: remove TriggersTab UI and AgentTrigger types - Tests: remove trigger config unit tests (no longer configurable) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: also remove agent tools config field Remove the tools field from agent configuration alongside triggers. The tools field was a placeholder — stored in the DB and shown in the UI but never passed to the daemon or used at runtime. - Database: migration 032 now also drops tools column - Backend: remove tools from create/update agent APIs and response - Frontend: remove ToolsTab UI, AgentTool type, and tools tab - Update landing page copy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): remove tools/triggers columns from test fixtures The test fixtures still referenced the dropped tools and triggers columns when inserting agent rows, causing CI failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
b94108768e |
feat: support multiple agents running concurrently on the same issue
- Relax ClaimAgentTask SQL constraint from per-issue to per-(issue, agent) serialization, allowing different agents to run in parallel on the same issue - Update GetActiveTaskForIssue API to return all active tasks (array) instead of just the first one - Refactor AgentLiveCard to render one card per active task, routing WebSocket messages by task_id for independent timelines - Fix shouldEnqueueOnComment to use per-agent dedup so a mentioned agent's pending task doesn't block the assigned agent's on_comment trigger Closes MUL-160 |
||
|
|
09764c5f51 |
feat(agent): replace hard delete with archive/restore (#346)
* feat(agent): replace hard delete with archive/restore
Replace agent deletion with soft archive pattern. Archived agents
are preserved in the database with all historical references intact
but cannot be assigned, mentioned, or trigger tasks.
Backend:
- Add archived_at/archived_by columns to agent table (migration 031)
- Replace DELETE /api/agents/{id} with POST /api/agents/{id}/archive
- Add POST /api/agents/{id}/restore endpoint
- ListAgents excludes archived by default (?include_archived=true to include)
- Skip archived agents in task triggers (on_assign, on_comment, on_mention)
- Block assignment to archived agents
- Cancel pending tasks on archive
- New events: agent:archived, agent:restored (replacing agent:deleted)
Frontend:
- Agent type includes archived_at/archived_by fields
- Mention autocomplete and assignee picker filter out archived agents
- Agent list shows archived agents with muted styling
- Agent detail shows archive banner with restore button
- Delete button replaced with Archive button and updated confirmation dialog
- API client: archiveAgent/restoreAgent replace deleteAgent
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(agent): self-review fixes for archive feature
- Fix: workspace store now fetches agents with include_archived=true
so archived agents are actually visible in the frontend (the archived
UI was dead code before — ListAgents excludes archived by default)
- Fix: add error logging for CancelAgentTasksByAgent in ArchiveAgent
- Fix: add idempotency guards — return 409 Conflict when archiving
an already-archived agent or restoring a non-archived agent
- Fix: revert unnecessary extra GetAgent query in ReconcileAgentStatus
(archived agents won't have running tasks after CancelAgentTasksByAgent)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
a80d61f8e1 |
fix(task): enforce per-issue serial execution in task claiming (#330)
Add NOT EXISTS check to ClaimAgentTask SQL to prevent claiming a queued task when the same issue already has a dispatched/running task. This ensures serial execution within an issue while preserving parallel execution across different issues (concurrency group pattern). Also add defensive guard in the frontend task:dispatch handler to avoid replacing an active task's LiveLog timeline mid-execution. Closes MUL-183 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
4780540bd2 | merge: resolve conflicts with main | ||
|
|
365b5b5f0e |
feat(agent): add task cancellation with stop button
Users can now interrupt running agents via a "Stop" button on the live
card. The daemon polls task status every 5 seconds and kills the agent
process when cancellation is detected.
Changes:
- New CancelAgentTask SQL query and CancelTask service method
- POST /api/issues/{id}/tasks/{taskId}/cancel endpoint
- Daemon polls GetTaskStatus during execution, cancels context on match
- Frontend: Stop button on AgentLiveCard, task:cancelled WS event
|
||
|
|
37881adbed |
feat(server): trigger agents via @mention in comments
When a user @mentions an agent in any issue's comment, the system now enqueues a task for that agent. The agent reads the issue context and replies to the triggering comment thread. Changes: - Add shared util.ParseMentions for mention parsing (used by both comment handler and notification listeners) - Add EnqueueTaskForMention to TaskService for explicit agent targeting - Add on_mention trigger type support in agent trigger config - Add HasPendingTaskForIssueAndAgent SQL query for per-agent dedup - Add enqueueMentionedAgentTasks in CreateComment handler Safety: prevents self-trigger (agent mentioning itself), dedup with assignee on_comment trigger, terminal issue status check, and per-agent pending task dedup. |
||
|
|
961de18c97 |
feat(agents): reply as thread instead of top-level comment (#205)
* feat(agents): reply as thread instead of top-level comment When an agent responds to a user comment, the reply is now nested under the triggering comment (parent_id) instead of appearing as a separate top-level comment. Also enables on_comment trigger by default for newly created agents. - Add trigger_comment_id column to agent_task_queue (migration 028) - Pass triggering comment ID through EnqueueTaskForIssue → task → createAgentComment - Include parent_id in WebSocket broadcast for agent comments - Default agent creation includes both on_assign and on_comment triggers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(cli): add --parent flag to comment add for threaded replies The agent posts comments via the CLI, so the correct fix is giving it a --parent flag rather than wiring trigger_comment_id through the task infrastructure. The agent reads the comment list, decides which comment to reply to, and passes --parent <comment-id>. - Add --parent flag to `multica issue comment add` - Update agent runtime instructions to explain --parent usage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(daemon): pass trigger_comment_id to agent execution context The agent now knows which comment triggered its task and gets an explicit instruction to reply to it using --parent. The trigger_comment_id flows from the DB through the claim response, daemon Task struct, and into issue_context.md where the agent sees it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(comments): agent replies to thread root, matching frontend behavior When the triggering comment is itself a reply (has parent_id), resolve to the thread root so the agent's reply stays in the same flat thread. This matches the frontend where all replies share the top-level parent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(cli): show parent_id and full IDs in comment list The table output now includes a PARENT column and shows full comment IDs (not truncated) so agents can see thread structure and use --parent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(daemon): instruct agents to always use --output json Agents now see explicit guidance to use --output json for all read commands, ensuring they get structured data with full IDs and parent_id for proper threading. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(daemon): differentiate comment-trigger vs assign-trigger context When triggered by a comment, the agent now gets clear instructions: - Primary goal is to read and respond to the comment - Do NOT change issue status just because you replied - Only change status if explicitly requested This prevents the agent from seeing "In Review" and stopping, since it now understands the task is to reply, not to re-evaluate the issue. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(daemon): split workflow by trigger type in CLAUDE.md/AGENTS.md The Workflow section in the agent's runtime config now shows a comment-reply workflow when triggered by a comment (read comments, find trigger, reply, don't change status) vs the full assignment workflow (set in_progress, do work, set in_review). Previously the agent always saw the assignment workflow, causing it to check the issue status, see "In Review", and stop without reading or replying to the triggering comment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(daemon): remove duplicate workflow from issue_context.md Workflow instructions now live only in CLAUDE.md/AGENTS.md (runtime_config.go). issue_context.md keeps just the task data: issue ID, trigger type, and triggering comment ID. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(task): skip duplicate comment on completion for comment-triggered tasks When triggered by a comment, the agent posts its own reply via CLI with --parent. The task completion path was also creating a comment from the agent's stdout output, resulting in duplicates. Now only assignment-triggered tasks auto-post output as a comment. Error messages from FailTask are still posted regardless of trigger type. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
1e2052c689 |
feat(agent): improve live output UI and add execution history
- Fix duplicate icons in tool call rows (use chevron only for expand/collapse) - Show detailed tool information (WebSearch queries, Agent prompts, Skill names) - Add thinking/reasoning rows with Brain icon and expandable content - Show tool results as separate chronological entries with previews - Add TaskRunHistory component for viewing past agent execution logs - Add listTasksByIssue API endpoint and task-runs route - Support thinking content blocks in agent SDK (MessageThinking type) - Improve callID→toolName mapping in daemon message forwarding |
||
|
|
3c93ebaf1c |
feat(agent): stream live agent output to issue detail page
When an agent is working on an issue, users can now see real-time output
in the issue detail page instead of waiting for completion.
Backend:
- Add task_message table and migration for persisting agent messages
- Add POST /api/daemon/tasks/{id}/messages endpoint for daemon to report
structured messages (tool_use, tool_result, text, error) in batches
- Add GET /api/daemon/tasks/{id}/messages for catch-up after reconnect
- Add GET /api/issues/{id}/active-task to check for running tasks
- Broadcast task:message events via WebSocket
- Daemon forwards agent session messages with 500ms text throttling
Frontend:
- Add AgentLiveCard component showing live tool calls, text output,
and progress indicators with auto-scroll
- Wire into issue detail timeline with WS subscription and HTTP catch-up
- Card appears when agent is working, disappears on completion/failure
|
||
|
|
9ede795c5b |
feat(api): strict workspace isolation + agent parity fixes
Enforce workspace isolation at every layer: - Router: move RequireWorkspaceMember middleware to group level so ALL workspace-scoped routes (issues, agents, skills, runtimes, inbox, comments) require workspace context - SQL: add GetXxxInWorkspace queries that filter by workspace_id, eliminating cross-workspace data access at the query level - Handlers: loadXForUser functions use workspace-scoped queries, no fallback to unscoped queries - Migration 025: add workspace_id column to comment table with backfill - ListComments: add workspace_id filter for defense-in-depth Fix daemon workspace mapping: - Server returns workspace_id in task claim response (from issue) - Daemon uses task.WorkspaceID directly instead of unreliable workspaceIDForRuntime() local map lookup - Remove workspaceIDForRuntime function Fix agent/human parity: - Comment update/delete: use resolveActor for isAuthor check so agents can edit/delete their own comments - Event attribution: replace hardcoded "member" with resolveActor in agent, skill, and subscriber publish calls Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
67f1f49b09 |
fix(daemon): prevent stuck tasks from blocking queue and add concurrent execution
- Expand FailAgentTask SQL to accept dispatched OR running status - Add FailStaleTasks server-side sweeper (dispatched >5min, running >2.5h) - Fix daemon handleTask to fail tasks on all error paths (StartTask, CompleteTask) - Make daemon poll loop concurrent with semaphore (default 20 parallel tasks) - Raise default agent max_concurrent_tasks from 1 to 6 (migration 023) - Add --max-concurrent-tasks CLI flag and MULTICA_DAEMON_MAX_CONCURRENT_TASKS env |
||
|
|
b112d1f1ae |
feat(tasks): add coalescing queue and task lifecycle guards
- Coalescing queue: use HasPendingTaskForIssue (queued/dispatched only) instead of HasActiveTaskForIssue so comments during a running task enqueue exactly one follow-up task that picks up all new comments. - Stale task cleanup: runtime sweeper now fails orphaned tasks when their runtime goes offline (daemon crash/network partition). - Cancel-aware daemon: handleTask checks task status after execution and discards results if the task was cancelled mid-run (e.g. reassign). - Terminal issue guard: ClaimTaskForRuntime auto-cancels pending tasks for done/cancelled issues instead of executing them. - Race condition safety net: unique partial index ensures at most one pending task per issue at the DB level. |
||
|
|
5b2c61cfab |
feat(agent): add instructions field for agent persona/identity
Add an `instructions` text field to the agent model, allowing users to define each agent's role, expertise, and working style. Instructions are injected into CLAUDE.md as an "Agent Identity" section so the agent knows who it is on every task execution. - Migration 021: add instructions column to agent table - Backend: create/update/get agent handlers support instructions - ClaimTask response includes instructions for daemon injection - execenv: inject instructions into CLAUDE.md meta-skill - Frontend: add Instructions tab to agent detail panel |
||
|
|
ffda18c809 |
feat(agent): add per-task session persistence for Claude Code resumption
Store the Claude Code session ID and working directory when a task completes. On the next task for the same (agent, issue) pair, look up the prior session and pass --resume <session_id> to Claude Code so the agent retains conversation context across multiple tasks on the same issue. Changes: - Migration 020: add session_id and work_dir columns to agent_task_queue - CompleteAgentTask stores session_id and work_dir on completion - GetLastTaskSession query retrieves prior session for (agent, issue) - ClaimTaskByRuntime handler populates prior_session_id in response - Daemon passes ResumeSessionID through to Claude backend Execute() - Claude backend adds --resume flag when ResumeSessionID is set |
||
|
|
4d8b2edb2d |
fix: address PR #149 review findings
- Replace custom contains/searchString with strings.Contains in tests - Fix variable shadow (r -> reposJSON) in workspace handler - Wire daemon auth token + server URL into spawned agent env vars - Remove unused CreateAgentTaskWithContext query (dead code after refactor) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
02df33803a |
feat: structured skills system with meta skill runtime injection
Replace agent.skills TEXT field with structured skill/skill_file/agent_skill tables. Skills are workspace-level entities with supporting files, reusable across agents via many-to-many bindings. Backend: migration 008, sqlc queries, CRUD handler, agent-skill junction, structured skill loading in task context snapshot. Daemon: meta skill injection via runtime-native config (.claude/CLAUDE.md for Claude, AGENTS.md for Codex) so agents discover .agent_context/ skills through their native mechanism. Lean prompt without inlined skill content. Frontend: Skills management page, agent Skills tab picker, SDK methods, TypeScript types, workspace store integration. Also removes auto-creation of init issues when creating agents. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
cdfa63af15 | feat(runtime): add local codex daemon pairing | ||
|
|
5a3a72c411 |
feat(server): add task service layer and daemon REST protocol
- Extract TaskService (server/internal/service/task.go) for task lifecycle: enqueue with context snapshot, claim, start, complete, fail, progress - Add daemon protocol endpoints under /api/daemon/: register, heartbeat, claim task, start/progress/complete/fail task - Task ↔ Issue status sync: running→in_progress, completed→in_review, failed→blocked - Agent status auto-management: reconcile idle/working based on running tasks - Enforce max_concurrent_tasks on task claiming (FOR UPDATE SKIP LOCKED) - Add UpdateIssueStatus query (fixes bug where UpdateIssue nulls assignee) - Extract shared pgx utils to server/internal/util/ to avoid circular imports - Migration 003: add context JSONB to agent_task_queue, daemon unique constraint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
1e61c1974c |
feat(server): implement full REST API with JWT auth and real-time WebSocket
- Add HTTP handlers for issues, comments, agents, workspaces, inbox, members, and activity - Implement JWT authentication middleware with Bearer token validation - Add sqlc queries for all entities (CRUD operations) - Extract router into reusable NewRouter() for testability - Expand SDK with full API client methods (CRUD for all resources) - Add updateWorkspace to SDK, add Member type to shared types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |