mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-30 16:20:35 +02:00
agent/lambda/aff7a16f
110 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
560e081d8f | Pass agent instructions inline to Hermes (#2283) | ||
|
|
97df9b90f5 |
refactor(daemon): rename repoCache interface, relax /health test timeout (#2270)
Two follow-up nits from PR #2211 review: - Rename the package-local `repoCache` interface to `repoCacheBackend` so the field declaration `repoCache repoCacheBackend` no longer shadows its own type name. - Bump the `/health`-must-respond timeout in `TestHealthHandlerRespondsWhileTaskRepoLookupWaits` from 200ms to 1s. The regression case blocks indefinitely on the old code, so a 1s upper bound still fail-fast detects it while leaving headroom for loaded CI runners. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f1dc3dc986 | fix: keep daemon health responsive during repo lookup (#2211) | ||
|
|
823f124d67 |
feat(daemon): extend GC to chat / autopilot / quick-create tasks (#2260)
* feat(daemon): extend GC to chat / autopilot / quick-create tasks
Before this change the daemon's GC was strictly issue-centric: only tasks
with a non-empty issue_id ever wrote .gc_meta.json, and shouldCleanTaskDir
called only the issue gc-check endpoint. Chat / autopilot run / quick-create
tasks fell through to the GCOrphanTTL mtime path, which mis-killed active
chat sessions while leaving deleted ones around far longer than necessary.
Schema:
- GCMeta gains a Kind discriminator and per-kind ID fields
(ChatSessionID / AutopilotRunID / TaskID). WriteGCMeta now takes a
GCMeta struct so the call site classifies the task explicitly.
- ReadGCMeta defaults empty Kind to GCKindIssue, so legacy on-disk meta
files keep flowing through the issue path with no migration required.
Server endpoints (siblings of /api/daemon/issues/{id}/gc-check, all behind
requireDaemonWorkspaceAccess for the same anti-enumeration shape):
- GET /api/daemon/chat-sessions/{id}/gc-check -> {status, updated_at}
- GET /api/daemon/autopilot-runs/{id}/gc-check -> {status, completed_at}
- GET /api/daemon/tasks/{id}/gc-check -> {status, completed_at}
shouldCleanTaskDir dispatches on Kind:
- chat: active is hard-skipped (no mtime fallback) so idle sessions are
never reclaimed; archived + GCTTL cleans; 404 falls back to mtime to
stay safe for cross-workspace tokens.
- autopilot_run: terminal (completed/failed/skipped/issue_created) +
GCTTL cleans; running/pending skips. Uses run.completed_at as the TTL
anchor since autopilot_run has no updated_at column.
- quick_create: terminal task status cleans immediately (workdir is not
reused by the linked issue task, which has its own envRoot); running
skips.
Also drops the "skipping .gc_meta.json: issue_id is empty" warn — with
the new kind dispatch, chat/autopilot/quick-create tasks now write a
proper meta file instead of triggering this log.
Refs: GC follow-up to PR #2077 (symptom fix) and #2115 (chat hard delete).
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): chat gc-check 404 cleans immediately, no mtime gate
PR review caught that the chat 404 path was routing through
orphanByMTime, which deferred reclamation to GCOrphanTTL (72h) when
acceptance #3 calls for cleanup within one GC cycle (≤ 1h) after the
user hard-deletes a session.
Every chat_session_id we ever ask about was written by this same daemon
under its current token, so the cross-workspace probe defense the issue
path needs doesn't apply here. Drop the gate and clean on 404 directly.
Test updates:
- TestShouldCleanTaskDir_KindDispatch/chat_404 flips the locked
expectation from gcActionSkip to gcActionClean.
- Adds TestShouldCleanTaskDir_ChatHardDeletedFreshMtime: GCOrphanTTL
set to a year so any mtime-based path is unmistakably out, and the
fresh-mtime workdir still cleans on the chat-404 fast path.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
6b7294aa5b |
fix(daemon): use brew prefix symlink for self-restart so Linux Cellar deletion does not orphan runtimes (#2076)
* fix(daemon): use brew prefix symlink for self-restart so Linux Cellar deletion does not orphan runtimes After brew upgrade on Linux, os.Executable() resolves /proc/self/exe to the Cellar path (e.g. .../Cellar/multica/0.2.9/bin/multica), which brew cleanup deletes. The previous IsBrewInstall() short-circuit skipped EvalSymlinks to 'preserve' the symlink, but on Linux there was nothing to preserve - the path was already resolved. Use cli.GetBrewPrefix() to resolve the stable symlink path <brewPrefix>/bin/multica for brew installs. Fall back to EvalSymlinks(os.Executable()) with a warning log when GetBrewPrefix() returns empty (brew binary missing from PATH). Introduce package-level function vars (isBrewInstall, getBrewPrefix) so the daemon test can override them without modifying the cli package. Closes #1624 * fix(daemon): harden brew-prefix fallback and document the WHY When `brew --prefix` is unavailable but the binary is under a known Cellar root, recover the prefix from cli.MatchKnownBrewPrefix and target <prefix>/bin/multica instead of falling back to the resolved Cellar path (which brew cleanup just deleted). - Extract knownBrewPrefixes + MatchKnownBrewPrefix in cli/update.go and reuse from IsBrewInstall to keep one source of truth for the install-root list. - Add a WHY comment above the brew branch in triggerRestart explaining the /proc/self/exe -> Cellar -> deleted-by-brew-cleanup chain. - Cover both fallback paths (matched / unmatched) in daemon_test.go. --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> |
||
|
|
ae985ae2a3 |
fix(daemon): tighten 404 task-not-found semantics — server + final guard (#2127)
* fix(server): return 500 for transient DB errors in daemon task lookup requireDaemonTaskAccess used to turn any GetAgentTask error into 404 "task not found", including transient DB connection / pool errors. Combined with PR #2107 — which added 404+"task not found" as a daemon cancellation trigger — that means a single DB hiccup could kill an in-flight agent run. Distinguish pgx.ErrNoRows (real "task gone", 404) from other errors (transient, 500 + warn log) using the existing isNotFound helper. Tests cover both paths via the mockDB pattern already used by TestFindOrCreateUserGating. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): honor task-deleted signal in post-runTask completion guard The final pre-completion check in handleTask only looked for status == "cancelled" and ignored errors. After PR #2107 added a 404 task-deleted cancellation path to the in-flight watcher, this trailing guard fell out of sync — if the task was deleted between the watcher's last poll and runTask returning, handleTask would still try to call CompleteTask and only learn about the deletion via the 404 from that callback. Reuse shouldInterruptAgent so the same truth table (cancelled OR 404 task-not-found, but NOT transient errors) drives both polling and the final guard. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b1be9ed27f |
fix(daemon): cancel running agent when task is deleted server-side (#2107)
When the server deletes a task while the daemon's agent is still running (issue removed, agent reassigned, workspace cleanup), GetTaskStatus starts returning 404 "task not found". The previous polling loop only checked for status == "cancelled" and silently swallowed the error, so the local agent kept emitting tool calls against a dead task until its own timeout fired — minutes of wasted model spend and patch_apply operations against a workdir nobody would consume. Changes: - Add isTaskNotFoundError next to isWorkspaceNotFoundError so the daemon can distinguish "task gone" 404 from "workspace gone" 404 (already handled separately) and from generic network errors. - Extract the cancellation polling goroutine in handleTask into watchTaskCancellation, plus a pure shouldInterruptAgent decision helper. The pure helper makes both signals (cancelled status and 404 task) easy to unit-test without spinning up a real backend. - Trigger interruption on the new 404 path. Transient errors (5xx, network) intentionally still don't cancel — the next poll will retry and a flaky link should not kill an in-flight agent. Tests cover the helper truth table, the existing "status cancelled" path, the new "task deleted (404)" path, and a negative case ensuring a running task is not interrupted. Co-authored-by: “646826” <“646826@gmail.com”> |
||
|
|
0dbfbfed2e |
fix(daemon/execenv): refuse to write .gc_meta.json when issue_id is empty (#2077)
A non-trivial fraction of completed task workdirs (~28% in field reports) end up with .gc_meta.json files containing issue_id: "". Empty issue_id defeats the daemon's own GC loop (gc.go:139 calls GetIssueGCCheck(meta.IssueID)) and external retention scripts that cross-reference issue status before deleting orphaned workdirs. Refuse to write the file when issueID is empty, logging a Warn so operators have a starting point for debugging the upstream race condition. Skip is preferred over a sentinel-marker file: it keeps the data invariant clean (a .gc_meta.json file always carries a valid issue_id) and matches the repo CLAUDE.md preference for not preserving dual-state behavior. WriteGCMeta now takes a *slog.Logger so it can emit the warning. The package already uses log/slog (Prepare/reuseEnv), and daemon.go:884 has taskLog in scope at the only call site. Closes #1913 Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> |
||
|
|
b08594f2f6 |
fix(daemon): isolate runtime poll & heartbeat schedules per runtime (#2116)
* fix(daemon): isolate runtime poll & heartbeat schedules per runtime A daemon serving multiple workspaces ran a single round-robin poll loop and a single HTTP heartbeat loop across every registered runtime. A 30s HTTP timeout for any one runtime serialized that delay across all the others — observed in production as one workspace's runtimes wedging every other workspace's runtimes on the same daemon. This change: - Replaces the shared runtime-set channel with a multi-subscriber watcher so taskWakeupLoop, heartbeatLoop, and pollLoop can each react to runtime-set changes independently. - Splits heartbeatLoop and pollLoop into supervisor + per-runtime worker goroutines. Each runtime owns its claim cadence and its heartbeat ticker, so a slow request on one runtime no longer blocks any other. - Stagers the per-runtime heartbeat first tick by a jittered delay up to one full interval to avoid a thundering herd at startup. - Sizes the WS writer channel to scale with the runtime count (max(16, 2*N)) so a full per-runtime heartbeat batch always fits; the previous fixed 8-slot buffer dropped heartbeats whenever a daemon watched more than ~8 runtimes. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): acquire execution slot only after ClaimTask, drain pollers before taskWG Two issues from review on the previous commit: 1. Acquiring the shared task slot before ClaimTask reintroduced the very head-of-line blocking the refactor was meant to remove. With MaxConcurrentTasks=1, a slow claim on one runtime parked the only slot for the duration of the HTTP timeout (up to 30s), starving every other runtime's claim attempts. Slots are now acquired after the claim returns a task; other runtimes' pollers stay free to claim. The already-dispatched task waits for a slot under MaxConcurrentTasks bounds, which is the same backpressure shape we had before. 2. pollLoop's shutdown path called taskWG.Wait immediately after cancelling pollers, but a poller could still be between ClaimTask returning a task and taskWG.Add(1). When taskWG's counter is zero that races with Wait — undefined sync.WaitGroup misuse, sometimes panic. Added a pollerWG so the supervisor blocks until every poller goroutine has actually returned before reaching taskWG.Wait. Tests: - TestRunRuntimePollerIsolatesSlowRuntime now uses MaxConcurrentTasks=1 (was 4) so it would have failed under the old slot-before-claim path. - New TestPollLoopShutdownWaitsForPollersBeforeTaskWG drives the exact race window — claim returns a task at the same moment shutdown fires — under -race. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): acquire slot before ClaimTask so capacity-waiters never enter dispatched The previous commit moved slot acquisition AFTER ClaimTask to address a review concern about head-of-line blocking with MaxConcurrentTasks=1. That introduced a strictly worse failure mode: server-side ClaimTask flips the task to `dispatched` immediately (agent.sql:174-176), and the runtime sweeper fails any task in `dispatched` for >300s with `failed/timeout` (runtime_sweeper.go:25-28). When local execution capacity is full and the next claimed task can't acquire a slot within 5 minutes, the user sees the exact failure this issue is fixing — `dispatched_at` set, `started_at` NULL, `failure_reason=timeout`. Reverted to slot-before-claim. The trade-off is the original review concern: with MaxConcurrentTasks=1 and a slow ClaimTask, other runtimes' claims are delayed by up to client.Timeout=30s. That's a 30s polling delay, not a failure — server-side those tasks remain `queued` (no timeout in that state) until a slot frees. 30s ≪ 300s, so other runtimes' tasks cannot get sweeper-failed because of this. The pollerWG fix from the previous commit (avoiding sync.WaitGroup misuse on shutdown) is preserved. Tests: - TestRunRuntimePollerIsolatesSlowRuntime: MaxConcurrentTasks back to 4 (the pre-issue baseline) — the headroom case where slot-before- claim still gives full per-runtime isolation. - New TestRunRuntimePollerSkipsClaimWhenAtCapacity: holds the only slot and verifies the poller never calls ClaimTask while sem is empty. The previous "claim first" path would have failed this. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c4352da126 |
fix(daemon): drain background repo syncs before test teardown (#2026)
TestRegisterTaskReposSurvivesWorkspaceRefresh started flaking on CI after #1988 (`feat: support repo checkout ref selection`) extended the bare-clone path to run an extra `git fetch` to backfill refs/remotes/origin/* under the new refspec layout. The race was already latent: registerTaskRepos kicks off `go syncWorkspaceRepos(...)` to clone a repo into the cache root, which in tests is `t.TempDir()`. Once the test waited on `repoCache.Lookup` to return a path it would proceed and return — but the bg goroutine was still inside `ensureRemoteTrackingLayout` running git operations on the clone dir. `t.TempDir`'s cleanup then races with those git commands and surfaces either as "directory not empty" or "fatal: cannot change to ... No such file or directory", with no hint that the failure is unrelated to the test's actual assertion. Track the background goroutine on the Daemon via a sync.WaitGroup and expose `waitBackgroundSyncs()` for tests. `newRepoReadyTestDaemon` registers a t.Cleanup that calls it, so every test that uses the helper now drains in-flight syncs before t.TempDir cleanup runs. No production-behavior change — registerTaskRepos still fires-and-forgets from the caller's perspective. Verified with `go test ./internal/daemon -run TestRegisterTaskReposSurvivesWorkspaceRefresh -count=30` (was failing within ~10 iterations before, 30 green after) and the full `go test ./internal/daemon/...` suite. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ba5b7db78e |
fix(server): persist ModelListStore across replicas via Redis (#2022)
* fix(server): persist ModelListStore across replicas via Redis The model picker uses a pending-request pattern: the frontend POSTs to create a request, the daemon pops it on its next heartbeat, runs agent.ListModels locally, and reports back. Until now the store was a plain in-memory map per Handler instance. That works for self-hosted single-instance deploys but fails in any multi-replica environment (Multica Cloud). Each replica has its own map, so: POST /runtimes/:id/models → request stored in replica A GET /runtimes/:id/models/<requestId> → polls land on B/C → 404 daemon heartbeat → only A sees PendingModelList POST .../<requestId>/result → daemon's report has to land on A Success probability ~1/N². The visible symptom is "No models available" in the picker for every provider, even those (Claude/Codex) whose catalog is statically populated end-to-end. Same shape of bug, same Redis-backed fix as multica-ai/multica#1557 did for LocalSkillListStore / LocalSkillImportStore. Reuse the operational playbook (namespaced keys, ZSET-backed pending queue, atomic ZREM+SET-running via the shared Lua script) so we don't introduce a second concurrency model for the same primitive. Changes: - Convert ModelListStore from struct to interface with context-aware methods. Add HasPending for cheap heartbeat-side probing. - InMemoryModelListStore — single-node fallback, used when REDIS_URL is unset (self-hosted dev / tests). - RedisModelListStore — multi-node implementation using the same key layout and Lua atomic claim as RedisLocalSkillListStore. - Use RunStartedAt (not UpdatedAt) as the running-timeout reference point, matching the local-skill stores so subsequent UpdatedAt bumps don't reset the running clock. - Heartbeat now uses the probe-then-pop pattern for the model queue (matching local-skills) so a slow Redis can't stall every connected daemon. Extends heartbeatMetrics + slow-log with probe_model_ms / pop_model_ms / probe_model_timed_out for parity. - Wire the Redis backend in NewRouterWithOptions when rdb != nil. - Tests for both backends. Redis tests gate on REDIS_TEST_URL so laptop runs without Redis still pass; CI provides it. Co-authored-by: multica-agent <github@multica.ai> * fix(server): persist RunStartedAt + retry model report on transient failures Two follow-ups from PR #2022 review: 1. RedisModelListStore was dropping ModelListRequest.RunStartedAt on persistence — the field is tagged json:"-" so it doesn't leak into the HTTP response, which made plain json.Marshal(req) silently discard it. Across-node readers saw RunStartedAt=nil and applyModelListTimeout's running branch became a no-op, so the 60s running-timeout escape hatch never fired. CI's TestRedisModelListStore_RunningTimeout was failing on this exact case. Fix mirrors RedisLocalSkillImportStore's envelope pattern — wrap in an internal struct that re-promotes the field. HTTP shape stays clean. Adds a no-Redis unit test that pins the round trip. 2. Daemon's handleModelList called d.client.ReportModelListResult directly and swallowed any 5xx, leaving the pending request stranded in "running" until its 60s server-side timeout — exactly the failure mode the multi-node store fix was meant to eliminate. Generalize the existing local-skill retry helper into reportRuntimeResultWithRetry (kind: model_list / local_skill_list / local_skill_import) and wire handleModelList through a new reportModelListResult helper. Renames the test-overridable var localSkillReportBackoffs → runtimeReportBackoffs to match. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f85b7cce91 |
fix: make CLI update completion status reliable (#2018)
Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1d1dedbf6e |
fix(daemon): reclaim disk on long-open issues + correct cancelled-status check (#1931)
* fix(daemon): reclaim disk on long-open issues + correct cancelled-status check Two related fixes for GitHub #1890 (self-hosted disk space growth): - The GC's done/cancelled branch compared `status.Status` against `"canceled"` (single l), but the issue schema and the rest of the daemon use `"cancelled"` (double l). Cancelled issues therefore never matched and only fell out via the 72h orphan TTL, which itself doesn't fire because cancelled issues are still reachable. Aligning the spelling lets cancelled-issue task dirs be reclaimed on the normal TTL path. - Add a third GC mode, artifact-only cleanup, for the common case the report flagged: an issue stays open for days while many tasks complete on it, so per-task `node_modules`, `.next` and `.turbo` directories accumulate without ever becoming GC-eligible. The new branch fires when `.gc_meta.completed_at` is older than `MULTICA_GC_ARTIFACT_TTL` (default 12h), the env root is not currently in use by an active task, and the issue is still alive. It removes only directories whose basename matches `MULTICA_GC_ARTIFACT_PATTERNS` (default narrow: `node_modules,.next,.turbo`); source, `.git`, `output/`, `logs/` and the meta file are preserved so subsequent tasks can still resume the workdir. Patterns containing path separators are dropped, `.git` subtrees are never descended into, symlinked matches are not followed, and every removal target is verified to live inside the task dir. Bookkeeping: `Daemon` now tracks active env roots with a refcounted set so the GC loop never reclaims a directory that is mid-execution; `runTask` claims the predicted root early plus the prior workdir on reuse paths. The cycle log is extended with bytes reclaimed and per-pattern counts so self-hosted operators can see what was freed. Docs: extend the daemon configuration table in CLI_AND_DAEMON.md with the new GC env vars and add a Workspace garbage collection section explaining the three modes and the artifact-pattern contract. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): protect active env root from full GC removal too Address GPT-Boy's PR #1931 review: the active-root guard only fired in the artifact-cleanup branch, leaving a real race on the full-removal paths. A follow-up comment on a long-done issue dispatches a task that reuses the prior workdir, but `CreateComment` does not bump issue.updated_at — so the issue still satisfies the done+stale GCTTL window and `gcActionClean` would `RemoveAll` the directory mid-execution. The orphan-404 path is similarly exposed when a token's workspace access is in flux. Move the `isActiveEnvRoot` check to the top of `shouldCleanTaskDir` so all three delete actions (clean, orphan, artifact) skip an in-use env root in one place, and drop the now-redundant guard from the artifact branch. Add tests covering the three at-risk paths: active root + done/stale issue, active root + 404 issue past orphan TTL, active root + no-meta orphan past TTL. Also align two stale comments noted in the same review: cleanTaskArtifacts now documents that symlinks are skipped entirely (the previous note implied the link itself was removed), and GCOrphanTTL no longer claims that 404s are cleaned immediately — the implementation gates them on the same TTL. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
da5dbc6224 |
refactor(repos): drop unused description + tighten create-project layout (#1930)
* refactor(repos): drop unused description + tighten create-project layout
Two related changes that touch the workspace-repos surface together.
1. Remove the per-repo `description` field everywhere it was threaded.
The only place it ever surfaced was a markdown table column the daemon
wrote into the agent runtime config, where most rows just read "—"
anyway. Agents already discover project structure by running
`multica project` / `multica issue` against the CLI, so the human-
readable description string carried no real value while taking up an
extra Settings input row and propagating through six layers (settings
UI → workspace.repos jsonb → handler RepoData → daemon RepoData →
repocache.RepoInfo → execenv.RepoContextForEnv).
- Settings → Repositories drops the description input; the URL field
now spans the whole row.
- WorkspaceRepo TS type loses `description`; backend RepoData /
RepoInfo / RepoContextForEnv all collapse to URL only.
- Daemon's runtime_config Repositories block changes from a
`| URL | Description |` markdown table to a simple bullet list.
- Tests updated; jsonb residue in existing workspaces is dropped at
normalize time, so no migration needed.
2. Tighten the Create Project modal footer: pull the Status / Priority /
Lead / Repos pills onto the same row as the Create Project button
(Linear-style single-row footer) instead of stacking them above it,
and swap the Repos pill icon from `FolderGit` to a real GitHub mark
(lucide-react v1 dropped brand icons, so the mark lives inline as a
small SVG component in this file).
I tried promoting Repos to its own "Resources" strip above the footer
to separate the resources abstraction from project metadata, but with
a single pill it looked too sparse — leaving a TODO comment in the
footer to revisit once we add Linear / Notion / Figma / Slack
resource types.
* fix(daemon test): drop residual Description field on RepoData literals
* fix(repos): drop Description residue surfaced after rebase on #1929
Project-resource github_repo lift path (#1929) and registerTaskRepos
both still constructed RepoData{...Description: ...} after the rebase.
Two test sites in daemon_test.go and execenv_test.go also reintroduced
the field. Strip them so the Description-removal change builds and
tests pass with the latest main.
|
||
|
|
2129aa3dee |
feat(projects): project github_repo resources override workspace repos (#1929)
* feat(projects): project github_repo resources override workspace repos When an issue's project has at least one github_repo resource, the daemon claim handler now sends only those as resp.Repos — workspace-level repos are hidden to avoid mixing two repo lists in the agent prompt. With no project github_repos (or no project), behavior is unchanged: workspace repos are surfaced as before. Lifts each project github_repo's url (and label, when present) into a RepoData entry so `multica repo checkout` and the meta-skill render the same URLs. The full structured list still ships at .multica/project/resources.json for skills that want everything. Adds TestProjectReposReplaceWorkspaceReposInMetaSkill covering the rendering side. Docs updated to spell out the new precedence. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): allow project repo URLs through the checkout allowlist When ClaimTaskByRuntime narrows resp.Repos to project github_repo URLs, the daemon receives URLs that may not exist in the workspace's GetWorkspaceRepos response. The existing checkout flow rejected those with ErrRepoNotConfigured because the allowlist (and cache) was built only from workspace-bound repos. Adds registerTaskRepos in daemon.runTask: before agent spawn, merge task.Repos into a new task-scoped allowlist (separate from the workspace-scoped one so a workspace refresh doesn't wipe project URLs) and kick off a background cache sync. ensureRepoReady now treats either allowlist as valid. Tests: - TestRegisterTaskReposAllowsProjectOnlyURL — project-only URL is checkout-able and does not trigger a workspace-repos refresh - TestRegisterTaskReposSurvivesWorkspaceRefresh — task URLs persist across refreshWorkspaceRepos - TestClaimTask_ProjectGithubReposOverrideWorkspaceRepos — claim handler returns only project repos when present, no workspace leakage - TestClaimTask_ProjectWithoutRepos_FallsBackToWorkspaceRepos — fall back to workspace repos when project has no github_repo resources Docs updated to spell out the daemon-side allowlist behavior. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b1345685a3 |
fix(task): rerun starts a fresh session, skip poisoned resume (#1928)
* fix(task): rerun starts a fresh session, skip poisoned resume
When a task ended in a known agent fallback ("I reached the iteration
limit and couldn't generate a summary.", "Put your final update inside
the content string. Keep it concise.") the (agent_id, issue_id) resume
lookup would still pick that session, so a manual rerun inherited the
poisoned state and reproduced the same bad output.
Two complementary guards:
1. Daemon classifies poisoned terminal output and routes it through the
blocked path with failure_reason set ('iteration_limit' /
'agent_fallback_message'). GetLastTaskSession excludes failed tasks
with those reasons, so even comment-triggered tasks no longer resume
them. Tasks that failed mid-flight (timeout, runtime_recovery, etc.)
are still resumable, preserving MUL-1128's auto-retry contract.
2. Manual rerun marks the new task force_fresh_session=true. The daemon
claim handler skips the resume lookup entirely when the flag is set,
capturing the user-intent signal that "the prior output was bad" even
when poisoned classification misses a future fallback wording.
Auto-retry of orphaned mid-flight failures (MaybeRetryFailedTask →
CreateRetryTask) does not take this path, so it keeps resuming.
Tests: classifyPoisonedOutput unit test; integration tests assert the
SQL filter excludes poisoned classifiers, RerunIssue flips the flag,
and the normal enqueue path leaves it false.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): cap poisoned-output matcher to short trimmed text
GPT-Boy review on MUL-1630: the previous strings.Contains match would
classify any output that quoted the marker substring — including a
review/analysis that simply discussed the marker itself. Real fallback
messages are short single-sentence affairs, so cap the candidate at
~one paragraph and trim whitespace before matching. Adds regression
tests covering a long quoting review and a marker buried in a long
real conclusion; both must stay classified as completed.
Co-authored-by: multica-agent <github@multica.ai>
* fix(migrations): rename 065 force_fresh_session → 066 to clear collision
main introduced 065_project_resources after this branch was cut, so
both files shared the 065_ prefix. The readiness check
(server/cmd/server/health.go → migrations.LatestVersion) takes the
last entry by lexical order, which is 065_project_resources, leaving
this branch's 065_force_fresh_session unguarded — a deploy that
applied project_resources but not force_fresh_session would still
report ready, and the next enqueue / rerun / claim would crash on
"column force_fresh_session does not exist".
Renaming to 066_force_fresh_session puts it strictly after
project_resources so readiness blocks until it's applied.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
44608713bb |
feat(projects): typed project resources + agent runtime injection (#1926)
* feat(projects): typed project resources + agent runtime injection
Adds a `project_resource` table that lets a project carry typed pointers
(github_repo today, more later) and surfaces them at agent runtime.
Server
- migration 065: project_resource (resource_type TEXT + resource_ref JSONB)
- sqlc CRUD + handler at /api/projects/{id}/resources
- claim handler attaches project_id/title + resources to issue tasks
Daemon
- TaskContextForEnv carries project context
- writes .multica/project/resources.json into workdir
- adds "## Project Context" block to CLAUDE.md / AGENTS.md / GEMINI.md
via type-dispatched formatter so new resource types just add a case
CLI
- multica project create --repo <url> attaches repos in one step
- multica project resource add/list/remove
Frontend
- Project create modal: Repos pill (workspace repos + ad-hoc URL)
- Project detail sidebar: collapsible Resources section with attach/remove
Docs
- New "Project Resources" chapter explaining the abstraction and
exactly what code to touch when adding a new resource type
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): transactional resources[] on create + generic CLI ref + test fix
Addresses review feedback on PR #1926:
1. CI red: TestProjectResourceLifecycle delete step called withURLParam
twice, which replaced the chi route context and dropped the project id.
Switched to the existing withURLParams helper from daemon_test.go.
2. POST /api/projects now accepts resources[] and attaches them in the
same transaction as the project. Invalid refs roll back the whole
create — no more half-attached projects on failure. Web modal + CLI
`project create --repo` both use the new bundled payload.
3. CLI `project resource add` now accepts a generic --ref '<json>' flag
so a new resource_type works without a CLI change. Per-type
shortcuts (--url for github_repo) remain as a convenience but are no
longer the only way in. Docs updated to drop the CLI from the
"files you must touch" list.
Adds two new server handler tests:
- TestCreateProjectAttachesResources (resources[] happy path)
- TestCreateProjectRollsBackOnInvalidResource (transactional rollback)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
924c69114d |
feat(daemon): expose concurrent task slot env (#1889)
* feat(daemon): expose concurrent task slot env * fix(daemon): address task slot review nits |
||
|
|
b6a3f8ed58 |
feat(daemon): add Co-authored-by trailer for Multica Agent to git commits (#1907)
* feat(daemon): add Co-authored-by trailer for Multica Agent to git commits Install a prepare-commit-msg hook in worktree bare repos that appends "Co-authored-by: multica-agent <github@multica.ai>" to every commit made by agents. Uses git interpret-trailers for proper formatting and skips duplicates. * feat(settings): add Co-authored-by toggle in workspace Labs settings Add a workspace-level toggle to enable/disable the Co-authored-by trailer for agent commits. Default is enabled (on). Backend: - Include workspace settings in daemon register response - Store settings in daemon workspaceState - Thread CoAuthoredByEnabled through WorktreeParams to conditionally install the prepare-commit-msg hook - Parse co_authored_by_enabled from workspace settings JSONB Frontend: - Replace empty Labs tab placeholder with a Git section containing a Switch toggle for the Co-authored-by trailer setting - Optimistically update the workspace query cache on toggle * chore(daemon): skip squash commits in Co-authored-by hook Test commit to verify the prepare-commit-msg hook appends the Co-authored-by trailer automatically. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
286ecf04b1 |
feat(daemon): add WebSocket heartbeat with HTTP fallback
Adds daemon WebSocket heartbeat acknowledgements while preserving HTTP heartbeat fallback and HTTP task claim/result paths. Keeps old daemon compatibility and task wakeup behavior intact. |
||
|
|
391a4ecd09 |
feat: add backend default agent args env vars (#1807)
* feat: add backend default agent args env vars * docs: document default agent args env vars |
||
|
|
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. |
||
|
|
9db91e89f5 |
feat: add daemon websocket task wakeups (#1772)
* feat: add daemon websocket task wakeups * feat: fan out daemon wakeups across nodes * fix: dedupe daemon wakeup loopback events * fix: lengthen daemon polling fallback interval --------- Co-authored-by: Eve <eve@multica.ai> |
||
|
|
6bd5bbad9c |
fix: timeout stalled Codex turns (#1730)
* fix: timeout stalled codex turns * fix: count codex progress events as activity |
||
|
|
2df969cffc |
fix(daemon): report cancelled tasks as "cancelled", not "timeout" (#1686)
When the server cancels a task (e.g. assignee changes during execution, explicit user cancel, or workspace_isolation check fail), the daemon's cancellation poll fires runCancel() on the run context. The drainCtx derived from runCtx then signals Done(), but executeAndDrain() was returning Status: "timeout" regardless of *why* the context ended. The "agent finished status=timeout" log line is then misleading — it suggests an actual deadline timeout when really the task was cancelled by upstream. We spent hours misdiagnosing a healthy handoff as a broken timeout because of this. Distinguish context.Canceled from context.DeadlineExceeded in executeAndDrain, and add a "cancelled" case to runTask so the status propagates through the existing log path. No behaviour change for genuine timeouts; no behaviour change for the cancelled-by-poll discard path in handleTask. Only the daemon log line and TaskResult.Status get the more accurate label. |
||
|
|
13d9d7df1b |
fix: pass autopilot run-only context to agents
Fix run-only autopilot tasks so agents receive autopilot context instead of empty issue instructions. Add regression coverage for run-only terminal event sync. |
||
|
|
cbe0cbef56 |
fix(daemon): retry local-skill reports on transient server errors (#1561)
Review follow-up on PR #1557: the server-side change started returning 500 when the store write failed, but the daemon's handleLocalSkillList / handleLocalSkillImport were discarding the ReportLocalSkill*Result error return. Net effect was a silent drop — the daemon moved on, the request stayed in "running" on the server, and the user saw the same "daemon did not respond within 30 seconds" timeout the store refactor was supposed to kill. Fix: route both report calls through reportLocalSkillResultWithRetry, which retries on 5xx + network errors with 0 / 0.5s / 2s / 4s backoff (total ~6.5s, well inside the 60s server-side running timeout), stops on 4xx (request expired / cross-workspace rejection — retry won't help), bails on context cancel, and logs Error on exhaustion so ops has a footprint to grep for. Tests (server/internal/daemon/local_skill_report_test.go, 6 new cases): - 500 twice then success -> 3 attempts, second retry lands - 404 -> exactly 1 attempt (permanent, no retry) - import 502 then success -> 2 attempts - All-500 -> burns through all backoff slots then gives up with ERROR log - Context cancel mid-backoff -> exactly 1 attempt, cancellation logged - Smoke: report paths hit /api/daemon/runtimes/<rt>/local-skills{,import}/<req>/result localSkillReportBackoffs is var-assignable so tests can swap in zero-delay schedules without paying real sleep latency. |
||
|
|
6fd1255873 |
feat(runtimes): remove Test Connection / runtime ping feature (#1554)
* feat(runtimes): remove Test Connection / runtime ping feature The Test Connection action invoked a real single-turn agent run to verify runtime connectivity. In practice it was expensive (reuses none of the normal task exec env, so it also gave misleading results) and low value — daemon heartbeat + Online status already covers the "is the runtime alive" question. Dropping the whole end-to-end probe path: - deletes server handler and in-memory PingStore - drops pending_ping from the heartbeat response and daemon poll loop - removes daemon.handlePing, PendingPing, ReportPingResult - removes the CLI `multica runtime ping` command - removes the PingSection UI block and RuntimePing types / api methods * docs: fix runtime CLI subcommand list in product-overview |
||
|
|
b624cd98ad |
feat: identify clients via X-Client-Platform/Version/OS (#1477)
* feat: identify clients via X-Client-Platform/Version/OS
Adds client identification headers (and matching WS query params) across
all first-party clients so the server can split logs/metrics/gating by
caller without parsing User-Agent.
- HTTP: X-Client-Platform, X-Client-Version, X-Client-OS
- WS: client_platform, client_version, client_os query params
- Platform ∈ {web, desktop, cli, daemon}; OS ∈ {macos, windows, linux}
Wired through the shared TS ApiClient/WSClient via a new identity option
on CoreProvider. Web reads its version from package.json/env; Desktop
captures version + OS synchronously in preload via sendSync IPC. Go CLI
and daemon clients populate the same headers using runtime.GOOS
(normalized darwin → macos).
Server-side adds a ClientMetadata middleware that stashes the headers in
request context; the request logger and logger.RequestAttrs surface them
on every access log and handler-level log. Realtime hub logs the same
fields on websocket connect.
CORS allowlist extended for the new headers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: address client-identity PR nits
- Memoize the CoreProvider identity object on Web and Desktop, and key
WSProvider's effect on identity primitives instead of the object
reference, so unrelated parent re-renders no longer tear down and
reconnect the WebSocket.
- Add direct header-injection tests for the CLI and daemon Go HTTP
clients (X-Client-Platform/Version/OS) and a normalizeGOOS unit test
on both packages.
- Add a TS test for WSClient that asserts client_platform/client_version/
client_os land on the upgrade URL and never leak the auth token.
- Add a hub test that dials the WS endpoint with client_* query params
and asserts the "websocket connected" log entry surfaces them as
structured attributes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
f247a4f544 |
feat(skills): import runtime local skills into workspace (#1431)
* feat(skills): import runtime local skills into workspace * fix(skills): address runtime local skill review feedback * docs(skills): annotate local provider skill paths --------- Co-authored-by: zhangliang <zhangliang@gaoding.com> |
||
|
|
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>
|
||
|
|
8eb81aa396 |
fix(daemon): enforce workspace isolation for agent execution (#1235) (#1260)
Phase 0 hotfix for the cross-workspace contamination reported in MUL-1027 / #1235: an agent running for workspace A ended up commenting on (and renaming) a two-day-old issue in workspace B. #1249/#1259 fixed resolution for autopilot tasks and consolidated the task-workspace resolver, and #1294 populated workspace_id in the claim response for run_only autopilot tasks. Those closed the known fallthroughs but the failure mode is still broader: whenever the daemon or server fails to supply a workspace, the CLI silently falls back to `~/.multica/config.json`, which is user-global, not workspace-scoped. On a host running daemons for multiple workspaces, a single gap in workspace propagation is enough to leak writes across workspaces. This PR adds three coordinated guards so no single layer's bug can cause a cross-workspace write: 1. `server/cmd/multica/cmd_agent.go` — `resolveWorkspaceID` detects the agent execution context (`MULTICA_AGENT_ID` / `MULTICA_TASK_ID` env, both daemon-only markers) and in that context refuses to fall back to the user-global CLI config. Human / script usage (no agent env) is unchanged: flag → env → config fallback chain still applies. 2. `server/internal/handler/daemon.go` — `ClaimTaskByRuntime` now captures the runtime's workspace from `requireDaemonRuntimeAccess` and enforces `resolved_task_workspace == runtime_workspace` after the existing issue/chat/autopilot branches. On mismatch or empty, the handler explicitly cancels the just-dispatched task (via `TaskService.CancelTask`, which also reconciles agent status) and returns 500. Without the explicit cancel, `ClaimTaskForRuntime` had already transitioned the task to 'dispatched' and the agent status to 'working', so a plain 500 would leave both stuck for the ~5 min stale-task sweep window. 3. `server/internal/daemon/daemon.go` — `runTask` refuses to spawn the agent when `task.WorkspaceID` is empty (defense-in-depth against server bugs and reused workdirs). Tests: - `cmd/multica/cmd_agent_test.go`: `TestResolveWorkspaceID_AgentContextSkipsConfig` — five subtests covering the full fallback matrix (outside agent context still reads config; agent context uses env; agent context with empty env returns empty; task-id-only marker also counts; requireWorkspaceID surfaces the agent-context error message). - `internal/handler/daemon_test.go`: `TestClaimTaskByRuntime_TaskWorkspaceMismatch_CancelsAndRejects` — constructs a data-inconsistent task (runtime_id in workspace A, issue_id in workspace B) and asserts the handler returns 500 AND leaves the task in 'cancelled' state (not 'dispatched'). Phase 1/2 follow-ups (prompt injection of workspace slug, session lookup workspace filter, cross-workspace audit of agent-facing endpoints, observability) are out of scope for this PR and tracked separately. |
||
|
|
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).
|
||
|
|
bd445782d5 |
fix(openclaw): stop passing unsupported flags and actually deliver AgentInstructions (#1362)
Fixes #1332. Two regressions introduced in #910 (2026-04-14, "OpenClaw backend P0+P1 improvements") that together block all openclaw users: 1. `openclaw agent` does not accept `--model` or `--system-prompt`, so any agent configured with a Model field crashed in ~700ms with `exit status 1`. Remove both forwards, and add them to openclawBlockedArgs so custom_args can't reintroduce the crash. Model is bound at registration time via `openclaw agents add/update --model`. 2. AgentInstructions were written to `{workDir}/AGENTS.md` by execenv.InjectRuntimeConfig, but openclaw loads bootstrap files from its own workspace dir — the file was never read, so every agent's Instructions field was silently discarded. Populate opts.SystemPrompt for the openclaw provider in runTask and prepend it to the `--message` payload in the backend so the model actually receives the instructions. Other providers surface instructions through their native runtime config file (CLAUDE.md / AGENTS.md / GEMINI.md) and are intentionally left unchanged to avoid double injection. Extract buildOpenclawArgs so arg construction is directly testable; add unit tests covering the removed flags, the SystemPrompt prepend, and custom_args filtering. |
||
|
|
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 |
||
|
|
a73336dcf8 |
feat(daemon): persistent UUID identity + legacy-id merge at register-time (#1220)
* feat(daemon): persistent UUID identity + legacy-id merge at register-time daemon_id is now a stable UUID persisted to `<profile-dir>/daemon.id` on first start, replacing the hostname-derived id that drifted whenever `.local` appeared/disappeared, a system was renamed, or a profile switched — each of which used to mint a fresh `agent_runtime` row and strand agents on the old one. To migrate existing installs without operator intervention, the daemon reports every legacy id it may have registered under previously (`host`, `host` with `.local` stripped, and `host[-profile]` variants for both). At register-time the server looks up each candidate row scoped to (workspace, provider), re-points its agents and tasks onto the new UUID-keyed row, records which legacy id was subsumed in the new `legacy_daemon_id` column for audit, and deletes the stale row. Result: users running `xxx.local`-keyed runtimes today transparently land on the new UUID row on next daemon restart. The hostname-prefix `MigrateAgentsToRuntime` / `daemon_id LIKE '...-%'` compatibility shim is no longer needed and has been removed along with the handler call that invoked it. * fix(daemon): handle bidirectional .local drift and case drift in legacy merge Review on #1220 flagged two gaps in the legacy-id migration candidate set: 1. Reverse .local: LegacyDaemonIDs only added the stripped variant when the current hostname ended in `.local`. The opposite direction — DB has `foo.local`, current host is `foo` — was missed, so runtimes registered under the `.local` variant stayed orphaned after upgrade. Now both variants (`foo` and `foo.local`) are always emitted, regardless of what `os.Hostname()` currently returns, plus their `-<profile>` suffix forms. 2. Case drift: os.Hostname() has been observed returning different casings on the same machine across mDNS/reboot state. A case-sensitive `=` comparison stranded rows like `Jiayuans-MacBook-Pro.local` when the daemon later reported `jiayuans-macbook-pro.local`. FindLegacyRuntimeByDaemonID now uses `LOWER(daemon_id) = LOWER(@daemon_id)` on both sides, so casing differences merge rather than orphan. The (workspace_id, provider) prefix still bounds the scan to a tiny set of rows so the non-indexed LOWER() comparison has negligible cost. Tests: TestLegacyDaemonIDs gets the mixed-case + reverse-direction cases; daemon_test.go adds TestDaemonRegister_MergesLegacyDaemonIDRuntime_ReverseDotLocal and TestDaemonRegister_MergesLegacyDaemonIDRuntime_CaseDrift. * fix(daemon): consolidate every case-duplicate legacy runtime, not just the first Follow-up review on #1220: after switching to `LOWER(daemon_id) = LOWER(@daemon_id)`, the single-row lookup still only merged one legacy row per candidate. If a machine already had two rows in the DB that differed only in casing (e.g. `Jiayuans-MacBook-Pro.local` AND `jiayuans-macbook-pro.local` coexisting because earlier hostname drift already minted a duplicate), only one of them got consolidated and the other stayed orphaned — violating the "no duplicate runtime per machine after backfill" acceptance. - FindLegacyRuntimeByDaemonID → FindLegacyRuntimesByDaemonID (:many) - mergeLegacyRuntimes iterates every returned row and dedupes across overlapping legacy candidates so `foo` and `foo.local` both resolving to the same stored row don't double-process Test: TestDaemonRegister_MergesAllCaseDuplicateLegacyRuntimes seeds two case-duplicate rows with one agent each and confirms both rows are deleted and both agents end up on the new UUID-keyed row. |
||
|
|
b5de04da59 |
fix(daemon): platform-aware Codex sandbox config to unbreak macOS network (MUL-963) (#1246)
* fix(daemon): platform-aware Codex sandbox config to unbreak macOS network On macOS, Codex's Seatbelt sandbox in workspace-write mode silently ignores '[sandbox_workspace_write] network_access = true' (see openai/codex#10390). That blocks DNS inside the sandbox, so 'multica issue get' and other CLI calls fail with 'dial tcp: lookup ...: no such host' — this is what caused MUL-963. Changes: - New server/internal/daemon/execenv/codex_sandbox.go: picks a sandbox policy based on runtime.GOOS and the detected Codex CLI version. Non-darwin or darwin with a known-fixed version keeps workspace-write + network_access=true; older darwin falls back to danger-full-access and logs a warn with upgrade hint. The fix-version threshold is a single constant (CodexDarwinNetworkAccessFixedVersion) so it's easy to bump once upstream ships. - Per-task config.toml now gets a 'multica-managed' marker block (BEGIN/END comments) rewritten idempotently; user-owned keys outside the markers are preserved. Legacy inline sandbox directives from earlier daemon versions are stripped on migration. - execenv.PrepareParams gains CodexVersion; execenv.Reuse takes a codexVersion arg; daemon.go caches detected versions at registration and threads them through to Prepare/Reuse. - Replaces the old ensureCodexNetworkAccess tests with platform-parameterised coverage (linux vs darwin, idempotency, legacy-migration, policy matrix). - docs/codex-sandbox-troubleshooting.md: symptom fingerprint table, decision matrix, self-check commands, trade-offs. Refs: MUL-963 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(daemon): hoist managed sandbox block above user tables (MUL-963) Review on #1246 flagged that upsertMulticaManagedBlock appended the managed block to EOF. If the user's config.toml ends inside a TOML table (e.g. [permissions.multica] or [profiles.foo]), a trailing bare sandbox_mode = "..." is parsed as a key of that preceding table, so Codex silently ignores the policy the daemon meant to apply. Two changes make the block position-independent: - renderMulticaManagedBlock now emits only top-level key=value lines and uses TOML dotted-key form (sandbox_workspace_write.network_access = true) instead of opening a [sandbox_workspace_write] header. The block therefore neither inherits from nor leaks into any surrounding table. - upsertMulticaManagedBlock always hoists the block to the top of the file (stripping any previously written managed block first), so the sandbox_mode line is always at the TOML root regardless of what the user put below it. This also migrates configs written by the original PR #1246 logic where the block was trapped behind a user table. Added tests for the regression scenario (pre-existing [permissions.*] table) and the legacy-trailing-block migration; updated the existing Linux default test and the troubleshooting runbook to reflect the dotted-key form. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: CC-Girl <cc-girl@multica.ai> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
6a2432b16b |
refactor: remove onboarding flow, fix daemon zero-workspace bootstrap (#1175)
* fix(daemon): allow startup with zero workspaces The daemon used to fail fast with "no runtimes registered" when the initial workspace sync returned zero workspaces. This masked a latent bug: a newly-signed-up user has no workspaces yet, so the daemon would crash immediately after login instead of waiting for the first workspace to be created. workspaceSyncLoop already polls every 30s (daemon.go:107, 365) to discover new workspaces — the fail-fast check at startup was bypassing this dynamic discovery. Remove the check so the daemon stays resident and picks up the first workspace whenever it appears. PR #1001 partially addressed this for the "server has workspaces but local CLI config is empty" case. This finishes the job for the true zero-workspace state, which until now was masked by the onboarding wizard always creating a workspace before the daemon started. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(views): extract CreateWorkspaceForm for reuse Modal and the upcoming /new-workspace page share the same form + mutation + slug validation. Extract to a shared component so they can't drift. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(views): add NoAccessPage for unknown or inaccessible workspace slugs Rendered when the URL slug doesn't resolve to a workspace the user has access to. Deliberately doesn't distinguish 404 vs 403 to avoid letting attackers enumerate workspace slugs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(paths): add /new-workspace route and reserve slug on both sides Adds paths.newWorkspace() builder, registers /new-workspace as a global (pre-workspace) prefix, and reserves the "new-workspace" slug on both frontend and backend (kept in sync per convention). Existing "onboarding" reservation retained — removing it would desync FE/BE and leaves no future fallback if an onboarding route is revived. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(migrations): audit no existing workspace uses 'new-workspace' slug Migration 046 blocks deploy if any workspace in the DB has slug = 'new-workspace', which would shadow the new global workspace creation route at /new-workspace. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add /new-workspace route on web and desktop Renders the CreateWorkspaceForm as a full-page workspace creation flow, used as the destination for first-time users with zero workspaces. Replaces the 4-step onboarding wizard with a single form. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: show NoAccessPage on unknown workspace slug, hold null during active removal Layouts render NoAccessPage when the URL slug doesn't resolve to an accessible workspace — except when the slug previously resolved during this layout instance's lifetime. URL and cache are two asynchronous signals: there will always be a short window where the URL still points at the old workspace but the cache has already been invalidated (e.g. just after a delete/leave mutation, or a realtime workspace:deleted event). Rendering NoAccessPage during that window would flash "Workspace not available" with recovery buttons in front of a user who just deleted the workspace themselves — jarring and wrong. useWorkspaceSeen classifies the two cases: - slug was seen before, now gone → user's intent is changing (caller is navigating away); render null, no flash - slug never seen → user is genuinely looking at an inaccessible workspace (stale bookmark, revoked access, link from a former teammate); render NoAccessPage with recovery options NoAccessPage deliberately does not distinguish 404 vs 403 to avoid letting attackers enumerate workspace slugs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: redirect zero-workspace users to /new-workspace instead of /onboarding Switches 8 call sites and the CLI: - Web: login, auth callback, landing redirect-if-authenticated - Desktop: routes.tsx IndexRedirect - Shared: dashboard guard, invite page fallback, workspace-tab on delete, realtime sync on workspace loss - CLI: cmd_login.go waitForOnboarding now opens /new-workspace Also adds /new-workspace to navigation store's lastPath exclusion list so it doesn't get persisted as a 'last visited' page. Adds a desktop App.tsx effect that restarts the daemon when workspace count transitions 0 → ≥1, so first-workspace creation triggers immediate daemon pickup rather than waiting up to 30s for the daemon's workspaceSyncLoop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove onboarding flow The 4-step onboarding wizard (workspace → runtime → agent → demo issues) is replaced by: - /new-workspace: a single-page workspace creation form (Phase 3) - NoAccessPage: explicit feedback when a slug doesn't resolve (Phase 4) - daemon zero-workspace bootstrap (Phase 1) so the daemon doesn't crash before the user creates their first workspace - desktop daemon restart on first workspace creation (Phase 5) for instant pickup instead of the 30s workspaceSyncLoop tick Deletions: - packages/views/onboarding/ (OnboardingWizard + 4 step components + tests) - apps/web/app/(auth)/onboarding/page.tsx - apps/desktop/src/renderer/src/components/onboarding-gate.tsx (+test) - OnboardingGate wrapper in desktop-layout.tsx - OnboardingRoute + /onboarding route in desktop routes.tsx - paths.onboarding() builder + /onboarding from GLOBAL_PREFIXES - packages/views/package.json onboarding export - /onboarding from navigation store's EXCLUDED_PREFIXES Retained (intentional): - 'onboarding' in RESERVED_SLUGS (both FE + BE) — kept for FE/BE sync and future-proofing if /onboarding is ever revived Also drops 4 demo issues that onboarding used to create on the new workspace ('Say hello', 'Set up repo', etc.). New workspaces are now fully empty; all list views already render empty-state UI correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: clean stale 'onboarding' references in comments and CLI helpers Batch cleanup of references to the removed onboarding flow: - 13 comment sites mentioning 'onboarding' updated to reflect the new /new-workspace flow or removed where no longer accurate - CLI waitForOnboarding renamed to waitForWorkspaceCreation (function name + docstring); behavior unchanged The 'onboarding' reserved slug entries (frontend + backend) are intentionally retained — see prior commit rationale. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(views): extract shared NewWorkspacePage shell The web (/new-workspace) and desktop (NewWorkspaceRoute) pages had identical outer layout — same container, heading, and copy — with only the onSuccess navigation primitive differing. That's exactly the No-Duplication Rule pattern: extract the shared UI, inject the platform-specific behavior. The apps now only own the thin auth guard (web needs it, desktop routes below WorkspaceRouteLayout already handle it) and the onSuccess → navigate call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove rollback compat layer and tighten daemon restart trigger Two cleanup items: 1. Drop localStorage['multica_workspace_id'] double-write in both workspace layouts. That write was added as a rollback safety net for the workspace-slug URL refactor (PR #1138) — the refactor has since landed and stabilized, so the compat shim is no longer needed. Per CLAUDE.md: don't keep compat layers beyond their purpose. 2. Tighten the desktop daemon-restart trigger. The previous ref-based logic fired a restart on any 0→1 workspace-count transition, including account switches (user A logout → user B login). Scope it precisely to 'this session started with zero workspaces and just gained one' using a three-state ref (null=undecided, true=empty-start, false=already-restarted-or-started-nonempty). Account switches are already handled by daemon-manager.ts on token change, so this avoids a redundant restart there. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): redirect to /login on logout and unauthenticated workspace visits Two gaps previously left users stuck on blank workspace pages: 1. app-sidebar logout() cleared all state but never moved the URL. The current path is /{workspaceSlug}/... which has no meaning without auth; the workspace layout would then see user=null, render null (via the hasBeenSeen short-circuit), and the user saw a blank page thinking logout didn't work. 2. The workspace layouts (web + desktop) had no !user handling at all. Any path that leaves user=null — token expiration, cross-tab logout, or fresh visit to a workspace URL without a session — resulted in the same blank screen. Fix: - app-sidebar.logout() explicitly push(paths.login()) after authLogout() to cover the primary (user-initiated) logout path. - Both workspace layouts get a defensive useEffect that redirects to /login whenever auth has settled and user is null. Covers token expiration, realtime logout, and any other silent session loss. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
a36252ca99 |
refactor(runtime): derive runtime usage from task_usage only (#1167)
* refactor(runtime): derive runtime usage from task_usage only
The daemon used to scan each runtime's local CLI log directory every 5
minutes (Claude Code, Codex, OpenCode, OpenClaw, Hermes) and post daily
aggregates to /api/daemon/runtimes/{id}/usage. Those directories are
shared with the user's own local CLI sessions, so the user's personal
usage was being counted as Daemon-executed usage. Cursor and Gemini had
no scanner at all, so their runtime-level aggregates were always zero.
Switch GetRuntimeUsage to aggregate task_usage (already scoped to
Daemon-executed tasks) via agent_task_queue.runtime_id. Single source of
truth; Cursor/Gemini/Copilot get runtime usage for free; no reliance on
external CLI log formats.
Removes:
- server/internal/daemon/usage/ (all scanners)
- Daemon.usageScanLoop + providerToRuntimeMap
- Client.ReportUsage
- ReportRuntimeUsage handler + POST /api/daemon/runtimes/{id}/usage
- UpsertRuntimeUsage / GetRuntimeUsageSummary queries
- runtime_usage table (migration 046)
Refs: MUL-786
* fix(runtime): bucket daily usage by task_usage.created_at, not enqueue time
ListRuntimeUsage was aggregating by DATE(atq.created_at) and filtering
on atq.created_at. agent_task_queue.created_at is the enqueue timestamp,
which drifts from actual token-production time: a task queued at 23:58
and executed at 00:05 was attributed to yesterday; a task sitting in
the queue overnight was counted on the queue day.
The ?days=N cutoff also became a rolling window (now() - N) instead of
a calendar-day boundary, silently clipping the morning of the earliest
day returned.
Switch bucket + filter to task_usage.created_at (~= task completion /
usage-report time) and snap the since cutoff to start-of-day via
DATE_TRUNC.
Add a regression test covering both scenarios: cross-midnight task
attributes to the day tokens were reported, and the earliest day's
pre-cutoff rows are still included.
|
||
|
|
df920e8641 |
fix(daemon): normalize repo URL and clarify reposVersion intent (#1090)
- TrimSpace incoming repoURL in ensureRepoReady to prevent unnecessary server refreshes when CLI passes URLs with whitespace - Add comment on reposVersion field clarifying it is stored for future version-based skip optimization - Add concurrency safety comment on syncWorkspacesFromAPI skip logic - Add test for URL trimming fast-path behavior |
||
|
|
0427fd8cc7 |
fix(daemon): refresh workspace repos on checkout miss (#1085)
Co-authored-by: black-fe <black-fe@gate.me> |
||
|
|
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 |
||
|
|
8030f1adbc |
feat(desktop): restart local daemon when bundled CLI version differs (#1041)
* feat(desktop): restart local daemon when bundled CLI version differs Desktop bundles a multica CLI binary at build time via bundle-cli.mjs. If a local daemon is already running from a previous session with an older CLI, the newly bundled version never takes effect until the user manually restarts. Fix that on the login/auto-start path. - Expose the daemon's CLI version on GET /health as cli_version (sourced from cfg.CLIVersion, which is already set from the ldflag at daemon startup in cmd_daemon.go). - In the desktop main process, query the resolved CLI binary's version once via `multica version --output json` and cache it for the process lifetime. - On daemon:auto-start, if the daemon is already running, compare the two versions. Restart only when BOTH sides are known and the strings differ — a restart kills in-flight agent tasks, so any uncertainty (bundled CLI unknown, older daemon without cli_version field, read failure) fails safe and leaves the daemon alone. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(daemon): defer version-mismatch restart until active tasks drain Previous iteration restarted the daemon immediately on a confirmed CLI version mismatch, which would kill any agent tasks mid-execution. Gate the restart on an active-task counter so in-flight work always finishes. - Daemon: add `activeTasks atomic.Int64` on the Daemon struct, increment/decrement it around handleTask, and expose it as `active_task_count` on GET /health. - Desktop: when a version mismatch is confirmed but active_task_count > 0, set a pendingVersionRestart flag instead of restarting. The 5s pollOnce loop retries ensureRunningDaemonVersionMatches on each tick and fires the restart the moment the count drops to 0. - Eventual consistency: if the user keeps the daemon permanently busy, the version stays out of date — that's a strictly better failure mode than silently killing hour-long agent runs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(daemon): cover version-check decision + /health counter exposure Addresses the test-coverage gap from the second review. - Go: extract the /health handler into a named method `(d *Daemon) healthHandler(startedAt time.Time)` so it can be exercised via httptest without spinning up a listener. Add health_test.go covering cli_version + active_task_count field exposure and the increment / decrement protocol used by pollLoop. - Desktop: extract the pure version-check decision logic into version-decision.ts (no electron, no I/O, no module state). The ensureRunningDaemonVersionMatches wrapper now delegates the "what should we do" decision to decideVersionAction and owns only the side effects (logging, flag mutation, restartDaemon call). - Desktop: bolt vitest onto apps/desktop (vitest.config.ts + catalog devDep + test script) so main-process unit tests have a home. Add version-decision.test.ts covering all four action branches and the busy→idle drain transition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(daemon): bust CLI version cache on retry-install, lock wire-level JSON keys Two polish items from review. - daemon:retry-install now also clears cachedCliBinaryVersion. Previously a retry that landed a newly-downloaded CLI at a different version would false-negative on the next version check because the cached version string was sticky for the process lifetime. - TestHealthHandlerReportsCLIVersionAndActiveTaskCount now decodes into a raw map[string]any and asserts the exact snake_case keys (cli_version, active_task_count, status). The desktop TS client keys on these literal strings, so a silent struct-tag rename must fail the test. Typed struct round-trip kept as a separate value check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
2cbebfc568 |
refactor(daemon): remove watch/unwatch workspace logic, default to all workspaces (#1003)
The daemon now automatically watches all workspaces the user belongs to, fetched directly from the API. This removes the manual watch/unwatch workflow, the config-based watched/unwatched lists, the /watch HTTP endpoints, the CLI watch/unwatch commands, and the desktop app's watched workspace UI and reconciliation logic. Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
6538496ee4 |
fix(daemon): sync workspaces from API before failing on empty runtime list (#1001)
When the CLI config has no watched workspaces (e.g. fresh desktop app install), loadWatchedWorkspaces returns successfully but registers zero runtimes. The runtime check immediately after fails with "no runtimes registered" before workspaceSyncLoop gets a chance to discover workspaces from the API. Run one sync cycle inline when the watched list is empty so the daemon can bootstrap itself without a pre-configured workspace list. |
||
|
|
40aa23a528 |
feat(desktop): daemon management panel with sidebar status bar (#952)
* feat(desktop): add daemon management panel with sidebar status bar Integrate multica daemon lifecycle management into the desktop app so users can start/stop/restart the daemon and view live logs without leaving the UI. Session tokens are automatically synced to the CLI config file, making daemon authentication transparent. - daemon-manager.ts: Electron main process module for daemon lifecycle (health polling, start/stop via CLI, token sync, log tail) - Preload bridge: new daemonAPI with IPC for all daemon operations - Sidebar bottomSlot: persistent daemon status indicator in sidebar footer (desktop-only, injected via AppSidebar slot) - Daemon panel Sheet: right-side drawer with status details, controls, and real-time log viewer with auto-scroll and level coloring - Token sync: on login and app startup, JWT is written to ~/.multica/config.json so daemon can authenticate seamlessly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(desktop): add P1+P2 daemon features — runtimes card, auto-start, settings P1: Runtimes page Local Daemon card - Add topSlot prop to shared RuntimesPage for platform injection - DaemonRuntimeCard shows status, agents, uptime with Start/Stop/ Restart/Logs buttons (desktop-only, injected via slot) P2: Auto-start and auto-stop - Daemon auto-starts on app launch when user is authenticated (controlled by autoStart preference, default: true) - Daemon auto-stops on app quit (controlled by autoStop preference, default: false — daemon keeps running in background by default) - Preferences persisted to ~/.multica/desktop_prefs.json P2: Daemon settings tab - New "Daemon" tab in Settings > My Account section (desktop-only) - Toggle auto-start and auto-stop behavior - CLI installation status check with link to install guide - SettingsPage gains extraAccountTabs prop for platform injection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): address PR review feedback on daemon management Must-fix: - before-quit handler now calls event.preventDefault(), awaits stopDaemon(), then re-calls app.quit() so the daemon actually stops before the app exits - Add concurrency guard (operationInProgress lock) in daemon-manager to reject overlapping start/stop/restart IPC calls - Extract shared types (DaemonState, DaemonStatus, DaemonPrefs), constants (STATE_COLORS, STATE_LABELS), and formatUptime to apps/desktop/src/shared/daemon-types.ts — all renderer components now import from this single source Should-fix: - Log viewer uses monotonic counter (LogEntry.id) instead of array index as React key, preventing full re-renders on overflow - All start/stop/restart handlers now show toast.error() with the error message when the operation fails - startLogTail retries up to 5 times with 2s delay when the log file doesn't exist yet (handles first-run case) Minor: - Cache findCliBinary() result after first successful lookup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(logger): suppress ANSI color codes when stderr is not a TTY Detect whether stderr is connected to a terminal and set tint's NoColor option accordingly. Previously daemon.log files contained raw escape sequences like \033[2m and \033[92m which made them unreadable in the Desktop log viewer and any non-TTY sink (docker logs, systemd, etc). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(daemon): runtime watch/unwatch HTTP endpoints and denylist Add GET/POST/DELETE /watch handlers on the daemon's health port so clients (notably Desktop) can add or remove watched workspaces at runtime without restarting the daemon or editing config.json. Each handler updates in-memory state under d.mu and persists back to ~/.multica/profiles/<name>/config.json for survival across restarts. - CLIConfig gains UnwatchedWorkspaces as an explicit opt-out denylist. syncWorkspacesFromAPI skips entries in the denylist so a manual unwatch isn't silently revived 30s later by the periodic sync. - loadWatchedWorkspaces tolerates an empty config and returns nil instead of erroring out, because Desktop starts daemons with a fresh profile and relies on the sync loop / watch endpoint to populate the list. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(desktop): bundled CLI, per-backend profile, and watch UI Make the Desktop app self-sufficient: it bundles its own multica binary, manages its own daemon profile keyed by the backend URL, and authenticates that daemon with a long-lived PAT it mints on first login. The daemon panel gains a checkbox list of watched workspaces and surfaces the active profile + server URL. CLI bootstrap - scripts/bundle-cli.mjs copies server/bin/multica into apps/desktop/resources/bin/ before electron-vite dev and electron-builder package. asarUnpack: resources/** already covers this path, so the binary ships with the .app in prod. - main/cli-bootstrap.ts adds an ensureManagedCli() fallback that downloads the latest release from GitHub when no bundled binary exists (first launch on a machine without developer tooling). - daemon-manager.resolveCliBinary prefers bundled > managed > download > PATH, so local iteration uses the freshly built binary. Daemon profile - resolveActiveProfile now derives a desktop-<host> profile name from the target API URL and creates its config.json on demand. Never reads or writes the user's hand-configured CLI profiles, avoiding the "Desktop polluted my default profile" class of bug. - syncToken detects a JWT input and exchanges it for a PAT via POST /api/tokens; caches the resulting mul_* token in the profile config so subsequent launches skip the round-trip. - startDaemon / stopDaemon / log tail all operate on the resolved profile; renderer sets the target URL via a new daemon:set-target-api-url IPC. Workspace watching - daemon-manager exposes daemon:list-watched / daemon:watch-workspace / daemon:unwatch-workspace IPCs backed by the daemon's new /watch endpoints. - App.tsx reconciles the user's workspace list against the daemon's watched set whenever TanStack Query updates it — new workspaces are registered instantly instead of waiting for the daemon's 30s sync, and removed workspaces are unwatched. - daemon-panel gains a "Watched Workspaces" section with per-workspace checkboxes that call watch/unwatch directly. Opt-outs persist in the profile's unwatched_workspaces denylist. Lifecycle states + UI - DaemonStatus gains `profile`, `serverUrl`, and an `installing_cli` state. Panel shows Profile / Server info rows and a "Setting up…" blurb during first-run CLI download; failure surfaces a Retry button. - Status bar renders a spinner during installation and hides the Start button until setup finishes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): register /onboarding route The create-workspace modal navigates to /onboarding on success, but the Desktop router only had flat routes (issues, projects, runtimes, etc.) — resulting in an "Unexpected Application Error! 404 Not Found" page after creating a new workspace. Mirror the web app's wiring: render OnboardingWizard with onComplete pushing to /issues, via the shared navigation adapter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(desktop): remove sidebar daemon status bar Drop the bottom-left daemon indicator in favor of the DaemonRuntimeCard at the top of the Runtimes page, which already shows the same info plus full Start/Stop/Restart controls and the Logs entry point. A single canonical place avoids fragmenting daemon status across the UI. Also remove the now-unused `bottomSlot` prop from AppSidebar — Desktop was the only consumer, Web never needed it, so keeping it would be dead scaffolding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): daemon panel layout and close button - Logs section now fills the remaining vertical space down to the sheet bottom instead of being capped at h-64, which left a huge empty area below it. Top section (status, actions, watched list) keeps natural height as shrink-0; the watched list gets its own max-h-48 scroll so a long list can't push Logs off screen. - Replace the Sheet's built-in close button with an explicit <button> wired directly to onOpenChange(false). The Base UI Dialog.Close wrapped in Button via the render prop wasn't firing on click in this panel; going straight through the controlled state guarantees it responds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): make daemon panel clickable inside Electron drag region The sheet opens at the top of the window, which visually overlaps the TabBar's -webkit-app-region: drag zone. Even though the sheet portals to document.body, Chromium computes drag regions over the final composited pixels, so the sheet inherited "drag" and swallowed the mouseup of every click (mousedown fired but click never resolved) — including the X close button. Mark the entire SheetContent popup with -webkit-app-region: no-drag to subtract it from the drag region. This also fixes future buttons / checkboxes inside the sheet that would have hit the same issue. While here, move the close button into the SheetHeader as a flex sibling of SheetTitle instead of an absolutely positioned overlay — simpler layout and avoids any stacking-context weirdness. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(desktop): clickable daemon runtime card row The whole Local Daemon row now opens the sheet panel — icon, title, and status line are all part of one click target. This replaces the standalone "Logs" button, which was redundant now that clicking anywhere on the row does the same thing. The right-side action cluster (Start / Stop / Restart) wraps its onClick in stopPropagation so pressing those buttons doesn't bubble up and open the panel. Keyboard access: Enter / Space on the focused row opens the panel, with a focus-visible background for feedback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(runtimes): mark Desktop-launched daemons as managed When the Multica Desktop app spawns the CLI it ships with, the resulting daemon shares its binary with the Electron bundle — Desktop is responsible for updating that binary on every release. Letting the daemon self-update would just get clobbered on the next Desktop launch and could brick the embedded binary mid-update. Propagate a "launched_by" signal end-to-end so the UI can hide the CLI self-update affordance (and the daemon refuses updates as a second line of defense): - Desktop's startDaemon spawns execFile with env MULTICA_LAUNCHED_BY=desktop. - daemon.Config gains LaunchedBy; cmd_daemon reads the env var on boot. - registerRuntimesForWorkspace includes launched_by in the request body. - Server DaemonRegister folds launched_by into runtime.metadata (JSONB — no migration needed). - handleUpdate returns a "failed" status with an explanatory message when LaunchedBy == "desktop", so even a bypass API call can't trigger the self-update path. - RuntimeDetail extracts metadata.launched_by and passes it to UpdateSection, which swaps the Latest / → available / Update button cluster for a muted "Managed by Desktop" label. CLI-only users (brew install, direct tarball) keep the exact same behavior — the env var is empty, the UI shows the update button, the daemon still self-updates on request. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): harden daemon manager from PR review - syncToken now takes userId and mints a fresh PAT on user switch, restarting a running daemon so it picks up the new credentials. A .desktop-user-id sidecar in each profile records the owner so a previous user's cached PAT can't be reused on the next login. - App.tsx wires onLogout on CoreProvider to daemonAPI.clearToken() and daemonAPI.stop() so the cached PAT and live daemon don't outlive the session. - startLogTail replaced with a cross-platform watchFile implementation (initial 32 KB window + poll for new bytes, handles truncation). spawn("tail") was broken on Windows. - writeProfileConfig now serializes through a promise chain to prevent concurrent writes from corrupting config.json. - startDaemon keeps the "starting" state until pollOnce confirms /health, avoiding a running → stopped flash when the Go daemon isn't yet listening after the supervisor returns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): verify downloaded CLI against checksums.txt Download goreleaser's checksums.txt alongside the release archive, parse the sha256 lookup, stream the archive through createHash, and refuse to install on mismatch or missing entry. Closes the supply- chain gap where auto-install would execute an unverified binary on first launch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(desktop): lint and style cleanups from PR review - eslint.config.mjs: add scripts/**/*.{mjs,js} override with globals.node so bundle-cli.mjs lints clean (was erroring on undefined process/console). - daemon-panel.tsx: log level classes now use semantic tokens (text-info, text-warning, text-destructive) instead of hardcoded Tailwind colors; escape the apostrophe in the retry copy. - daemon-settings-tab.tsx: import DaemonPrefs from shared/daemon- types instead of redefining it. - runtimes-page.tsx: fix indentation inside the new topSlot wrapper. 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> Co-authored-by: yushen <ldnvnbl@gmail.com> |
||
|
|
0a998d1cef |
Merge pull request #846 from multica-ai/agent/j/feb218fd
feat(agent): support custom environment variables for router/proxy mode |
||
|
|
977dc6479d |
fix(daemon): prevent task stall when agent process hangs on stdout (#947)
When an agent CLI process hangs (e.g. a tool call blocks on unreachable I/O), the daemon's scanner blocks indefinitely on stdout, preventing the Result from ever being sent. This causes tasks to stay in "running" state permanently with no further events. Three-layer fix: 1. Agent backends (claude, opencode, openclaw, gemini): add a watchdog goroutine that closes the stdout/stderr pipe when the context is cancelled, forcing the scanner to unblock. Also set cmd.WaitDelay so Go force-closes pipes after 10s if the process doesn't exit. 2. daemon executeAndDrain: add an independent drain timeout (backend timeout + 30s buffer) with context-aware select on both the message channel and the result channel, so the daemon never blocks forever. 3. daemon ping path: add context-aware select so pings don't deadlock if the agent backend stalls. Closes #925 Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
1d71df8622 |
fix(daemon): include dispatched agent identity in CLAUDE.md (#877)
When an agent is triggered via @mention (not as the issue assignee), the generated CLAUDE.md had no explicit agent identity. The agent would infer its identity from the issue's assignee field, causing it to skip work intended for it. Now CLAUDE.md always includes "You are: <agent-name> (ID: <agent-id>)" so the agent knows exactly who it is regardless of the issue assignee. Closes MUL-709 |