mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 06:39:01 +02:00
v0.2.13
136 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
632fdde700 |
fix(cli): keep Windows daemon alive after terminal closes + unblock multica update (#1420)
* fix(cli): detach daemon from parent console on Windows CREATE_NEW_PROCESS_GROUP alone leaves the daemon attached to the parent console, so closing the launching cmd/PowerShell window fires CTRL_CLOSE_EVENT down the inherited console and takes the daemon with it. Add DETACHED_PROCESS so the child has no console at all; stdout/stderr are already redirected to the log file before spawn. * fix(cli): make `multica update` work while the binary is running on Windows On Windows, a running .exe is opened without FILE_SHARE_WRITE, so the previous os.Rename(tmp, exe) always failed with "Access is denied" — every `multica update` on Windows hit this, because the CLI is updating its own running binary. Windows does allow renaming the running .exe (just not overwriting it), so the new Windows-only replaceBinary moves the running binary to `.old` first, installs the new one, and restores the original if installation fails. A best-effort CleanupStaleUpdateArtifacts runs at CLI/daemon startup to reclaim the leftover `.old` file once the old process has exited. Unix keeps the plain rename-over semantics (the old inode stays valid for the running process). * fix(cli): stop daemon via HTTP /shutdown instead of console ctrl events With DETACHED_PROCESS the Windows daemon shares no console with the stop caller, so `GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid)` silently never reaches it — the old code would report "stop sent" while the daemon kept running. Replace the platform-specific stopDaemonProcess with a cross-platform POST to the daemon's HTTP /shutdown endpoint, which cancels the same top-level context the self-restart path already uses. Fall back to `process.Kill()` if the HTTP call fails. Also drops the now-unused stopDaemonProcess / CTRL_BREAK_EVENT wiring, adds handler tests, and updates the DETACHED_PROCESS comment. |
||
|
|
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. |
||
|
|
9e47b83f02 |
feat(agent): add Kimi CLI as agent runtime (#1400)
* feat(agent): add Kimi CLI as agent runtime
Adds support for Moonshot AI's Kimi Code CLI (https://github.com/MoonshotAI/kimi-cli)
as a new agent runtime, alongside Claude, Codex, OpenCode, OpenClaw, Hermes,
Gemini, Pi, Cursor and Copilot.
Kimi Code CLI implements the standard Agent Client Protocol (ACP) via the
`kimi acp` subcommand, so the new `kimiBackend` reuses the existing
hermesClient JSON-RPC transport in the agent package — only the binary,
client identity, log prefix, and tool-name extraction differ.
Wiring:
- server/pkg/agent: new kimiBackend + kimi_test.go; registered in New(),
LaunchHeader map, and the supported-types coverage test.
- server/internal/daemon/config.go: probes `kimi` (overridable via
MULTICA_KIMI_PATH / MULTICA_KIMI_MODEL).
- server/internal/daemon/execenv: writes AGENTS.md as the runtime context
file (Kimi reads AGENTS.md natively via /init), and writes skills under
`.kimi/skills/` so they are auto-discovered by the project-level skill
loader.
- packages/views/runtimes: ProviderLogo gains a Kimi mark.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(agent/kimi): support per-agent model selection via ACP set_model
Wire Kimi into the model dropdown introduced in #1399:
- ListModels gets a 'kimi' case that drives the same ACP
initialize + session/new handshake as Hermes; both share a new
discoverACPModels helper and parseACPSessionNewModels parser
so future ACP backends only need a small provider entry.
- kimiBackend now issues session/set_model after session/new when
opts.Model is non-empty, mirroring the Hermes flow. Failures
fail the task instead of silently falling back to Kimi's
default model — silent fallback would hide that the dropdown
pick wasn't honoured.
Verified: go build ./..., go test ./pkg/agent/... ./internal/daemon/... ./internal/handler/..., pnpm typecheck and pnpm test (138 passed).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(agent): address code review feedback on Kimi runtime
- Share ACP provider-error sniffer between hermes and kimi. Previously
only hermes promoted stderr-observed 4xx/5xx into a failed task;
kimi would report "completed + empty output" when the Moonshot
upstream rejected a request (expired token, rate limit, …). Rename
hermesProviderErrorSniffer → acpProviderErrorSniffer and parameterise
the provider name; wire it into kimiBackend.Execute the same way.
- Rename extractHermesSessionID → extractACPSessionID (shared by all
ACP backends) so the name matches parseACPSessionNewModels.
- Drop the redundant second argument to kimiToolNameFromTitle; the
Message struct has only one relevant field (Tool), so passing it
twice was a dead fallback. Document that the function normalises
residual capitalised kimi titles not caught by hermesToolNameFromTitle.
- Remove kimi-only cmd.WaitDelay override; the hermes baseline is
fine for both and divergence adds noise.
- Add TestKimiBackendSetModelFailureFailsTask: fake `kimi acp` binary
that returns a JSON-RPC error for session/set_model, asserts that
the task result surfaces status=failed with the model name + upstream
message and preserves the session id.
- Fix stale agent listings in agent.go / daemon/config.go doc comments
(missing cursor, gemini, copilot).
All: `go build ./...`, `go vet ./...`, `go test ./pkg/agent/...
./internal/daemon/... ./internal/handler/...` green.
* fix(agent/kimi): pass --yolo so Shell tools don't hang on approval
Kimi's default config has `default_yolo = false`. Every Shell/file-mutating
tool call causes kimi acp to send a `session/request_permission` request
and block (up to 300s) waiting for a response. The daemon's hermesClient
only handles `session/update` notifications — permission requests go
unanswered, the tool call times out, and the UI loop eventually dies
("UI loop timed out"). Observed with the first real kimi task: agent sat
as Live for ~7 minutes before the daemon killed it.
The fix mirrors hermes' HERMES_YOLO_MODE=1 override: pass `--yolo` to
`kimi` so it auto-approves everything. `--yolo` is a top-level flag on
the `kimi` CLI (not a flag on `kimi acp`), so it must come before the
`acp` subcommand in argv. Added to kimiBlockedArgs so user custom_args
can't strip it.
While here, fix a related bug that made kimi tool names show up empty
in the daemon log ("tool #1: "): hermesToolNameFromTitle's fallback
returned `kind` when neither title-with-colon nor kind matched a known
tool. Kimi's ACP `tool_call` emits bare titles like "Shell" or "Read
file" with no `kind` at all, so we'd drop the title on the floor before
kimiToolNameFromTitle ever got a chance to map it. Now: preserve the
title when kind is unclassified; hermes titles always carry a colon so
this branch never fires for hermes.
Tests:
- TestKimiBackendPassesYoloFlag — fake binary that records its argv,
asserts --yolo comes before acp.
- TestHermesToolNameFromTitle rows for bare kimi-style titles.
- Existing suite green: go build, go vet, full pkg/agent + daemon +
handler test packages.
* fix(agent/acp): auto-approve session/request_permission from agent
The previous attempt (`kimi --yolo acp`) was a no-op. Inspected the
kimi-cli source: the `acp` Typer subcommand takes no parameters, so
flags on the root `kimi` command are dropped before `acp_main()` runs
— it's impossible to opt into YOLO mode through CLI flags for ACP.
The real fix is on our side: respond to session/request_permission.
ACP is bidirectional. When kimi runs a Shell or file-write tool, it
sends `session/request_permission` (agent → client, JSON-RPC request
with id + method) and waits up to 300s for a response. Our existing
hermesClient.handleLine only dispatched: (id + result/error) →
handleResponse, and (no id + method) → handleNotification. A request
with BOTH id and method fell through and got silently dropped — kimi
timed out, UI loop died, task sat stuck for 7 minutes.
Add handleAgentRequest: for session/request_permission, echo the id
and respond with outcome=selected, optionId=approve_for_session. The
daemon is headless; there's no user to prompt. `approve_for_session`
lets the agent remember the action so subsequent identical calls
(every Shell, every file write) skip the round-trip entirely. For any
other agent → client method, reply with standard -32601 method-not-
found so the agent doesn't block.
Also:
- Add writeMu so request() (main goroutine) and handleAgentRequest
(reader goroutine) don't interleave JSON frames on stdin.
- Revert the `--yolo acp` flag — it's a no-op, and carrying it in
kimiBlockedArgs gives the wrong impression that it does something.
Comment in kimi.go now points at handleAgentRequest as the real fix.
Tests:
- TestHermesClientAutoApprovesPermissionRequest: inject a
session/request_permission, assert the reply echoes the id and
carries {outcome: selected, optionId: approve_for_session}.
- TestHermesClientReplesMethodNotFoundForUnknownAgentRequest: confirm
unknown agent → client methods get JSON-RPC -32601 instead of silence.
- TestKimiBackendInvokesACPSubcommand replaces the yolo-flag assertion
with a negative assertion: no dead --yolo / --auto-approve / -y on
argv, since they'd pretend to do something they can't.
All: go build ./..., go vet ./..., go test ./pkg/agent/... green.
* fix(agent/acp): surface kimi tool input/output via content blocks
Kimi-cli emits tool_call and tool_call_update ACP frames with the
input/output inside a `content` array of ContentToolCallContent
blocks (shape: {type:"content", content:{type:"text", text:"..."}}),
not in the hermes-style `rawInput` map / `rawOutput` string. Our
parser only looked at rawInput/rawOutput, so the daemon recorded
empty Input and Output for every kimi tool — the execution-history
UI showed blank terminal panels even for commands that ran fine.
Add extractACPToolCallText() and a fallback in handleToolCallStart /
handleToolCallUpdate: when rawInput is nil / rawOutput is empty, pull
the text out of the content blocks. rawInput / rawOutput still take
precedence so hermes' behaviour is untouched. Terminal /
FileEditToolCallContent blocks are skipped (we have nothing to render
them as — kimi only emits TerminalToolCallContent when the client
advertises terminal capability, which we don't).
Tests:
- TestHermesClientHandleToolCallStartKimiContent — content array →
Input.text populated.
- TestHermesClientHandleToolCallCompleteKimiContent — multi-block
content → Output concatenated with newline separator.
- TestHermesClientHandleToolCallRawOutputTakesPrecedence — hermes
rawOutput still wins when both are present.
- TestExtractACPToolCallText — unit coverage for the helper
(single/multiple text blocks, terminal-block skip, empty input).
* fix(agent/acp): buffer streaming tool args so Input isn't empty in UI
kimi-cli streams tool args token-by-token via tool_call_update frames
— the initial tool_call carries an empty content block and each
subsequent in_progress update carries the cumulative JSON so far
(`{`, `{"comma`, `{"command": "echo`, …). The final completed update
then carries the tool's stdout, not the args. Observed per kimi-cli
acp/session.py::_send_tool_call{,_part,_result} and confirmed by
driving a real Shell call end-to-end: 10 in_progress frames, last
with `{"command": "echo hello world"}`, then completed with `hello
world\n`.
Our previous handleToolCallStart emitted MessageToolUse on the first
tool_call frame, capturing the empty content — so every kimi tool
appeared in the execution-history UI with a blank input. Output was
correct (fix
|
||
|
|
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).
|
||
|
|
c76c790b32 |
fix(daemon/execenv): make posting result comment an explicit workflow step (#1372)
Agents were silently finishing tasks without ever posting results to the issue — their final reply stayed in terminal/log output only. See MUL-1124. Root cause: the injected CLAUDE.md / AGENTS.md put "post a comment with results" inside the body of step 4 (a nested clause in the default workflow description), so skill-driven flows jumped straight from "do the work" to `status in_review`. - Hoist posting the result comment into its own explicit, numbered step in both assignment-triggered and comment-triggered workflows, with the exact `multica issue comment add` invocation inlined. - Add a hard warning at the top of the Output section that terminal / chat text is never delivered to the user. - Add regression test covering both workflow branches. |
||
|
|
951f51408a |
fix(agent/comments): prevent resumed sessions from reusing stale --parent UUID (#1374)
* fix(agent/comments): re-emit trigger comment id every turn + server-side parent_id guard Resumed Claude sessions keep prior turns' tool calls in context, so a comment-triggered task could reuse the PREVIOUS turn's --parent UUID instead of the current trigger's. The reply landed in the wrong thread (MUL-1125): backend stored exactly what the agent sent, but the agent pulled a stale UUID from its own conversation memory. Two layers of defense: 1. Extract BuildCommentReplyInstructions so daemon.buildCommentPrompt and execenv.InjectRuntimeConfig emit the same "use this exact --parent, do not reuse values from previous turns" block. The per-turn prompt now carries the current TriggerCommentID, which it previously relied on CLAUDE.md for (and CLAUDE.md isn't re-read mid-session). 2. Handler-side guard in CreateComment: when an agent posts from inside a comment-triggered task (X-Agent-ID + X-Task-ID, task has TriggerCommentID), require parent_id == task.TriggerCommentID or return 409. Assignment-triggered tasks are untouched. * fix(agent/comments): scope parent_id guard to the task's own issue Two issues from CI + GPT-Boy's review: 1. Guard was too broad: the CLI stamps X-Task-ID on every request, so an agent legitimately commenting on a different issue while its current task was comment-triggered would get 409'd with the wrong issue's trigger comment id. Narrow the guard to fire only when the request's issue matches the task's own issue — cross-issue agent activity stays unblocked. 2. The integration test tried to insert a second queued task for the same (agent, issue), which hits the idx_one_pending_task_per_issue_agent unique index. Replace the assignment-triggered-task sub-case with a cross-issue regression test (the scenario we now need to cover anyway): post on issue B while X-Task-ID points at a comment-triggered task on issue A, expect 201. |
||
|
|
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> |
||
|
|
e198a67f8f |
docs(prompt): warn agents that mention syntax is an action, not a text reference (#1306)
Agent mentions enqueue a new task; member mentions send a notification. Without this warning, agents have used `[@Name](mention://agent/<id>)` in prose (e.g. "GPT-Boy is correct") and accidentally re-triggered the agent. Adds a caveat under `## Mentions` in the prompt injected into agent runtimes, plus tightens the Agent bullet to make the side-effect explicit. |
||
|
|
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 |
||
|
|
b2307a5ee9 |
fix(execenv): write Copilot skills to .github/skills/ for native discovery (#1270)
GitHub Copilot CLI scans project-level skills from .github/skills/<name>/SKILL.md (per the official cli-config-dir-reference docs), not from .agent_context/skills/. Previously, skills injected for the copilot provider were placed under .agent_context/skills/ and only referenced by name in AGENTS.md, meaning Copilot would not actually pick them up. - resolveSkillsDir: add a dedicated copilot case writing to .github/skills/ - Update doc comments in context.go and runtime_config.go - Add TestWriteContextFilesCopilotNativeSkills covering the new path and ensuring .agent_context/skills/ is not created for copilot Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
4bd8533269 |
fix(daemon): machine-scoped daemon.id so CLI + desktop share one identity (#1263)
Before this PR, `EnsureDaemonID(profile)` wrote to ~/.multica/profiles/ <profile>/daemon.id — meaning the same physical machine minted a different UUID per profile. On any host running both the CLI-spawned daemon (default profile) and the desktop-spawned daemon (profile derived from API host), that produced two runtime rows per provider per workspace. The server-side `legacy_daemon_ids` merge only covers hostname variants, not UUIDs, so the rows just piled up. Profile boundaries are about which backend/account the daemon is talking to, not about the physical machine. Identity should be per-machine, token should be per-profile. Changes: - `EnsureDaemonID` now always reads/writes ~/.multica/daemon.id regardless of the `profile` argument. The argument is retained for migration-only use (see promotion below). - Migration path: when the canonical file is missing and the requested profile has a pre-change per-profile daemon.id, promote that UUID in place so a user who only ever ran under a named profile keeps the same identity instead of minting a fresh UUID and round-tripping a merge. - New `LegacyDaemonUUIDs()` scans ~/.multica/profiles/*/daemon.id and returns every UUID that survives parsing. `config.go` now appends those to the daemon's `legacy_daemon_ids` payload, so any runtime rows previously registered under a per-profile UUID (on any backend) get merged into the canonical machine UUID at register time. Tests replace the `ProfileIsolated` assertion with `SharedAcrossProfiles` and add coverage for promotion, UUID scanning (including skipping corrupt files), and the empty-profiles-dir fast path. |
||
|
|
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. |
||
|
|
9e15b17c92 |
feat(cli): add autopilot commands (#1234)
* feat(cli): add autopilot commands Expose the existing autopilot REST API through the multica CLI so users and agents can list, get, create, update, delete, trigger, and inspect autopilots, plus manage their triggers (schedule/webhook/api). Also surface the read + core write commands in the agent meta skill prompt so agents discover them without needing --help. - new cmd_autopilot.go (+ test) wiring /api/autopilots endpoints - add APIClient.PatchJSON (autopilot update uses PATCH) - expose autopilot in CORE COMMANDS group - extend runtime_config.go meta skill with autopilot entries - document autopilot command group in CLI_AND_DAEMON.md * fix(autopilot): address code review — restrict run_only, validate workspace on update Code review caught two issues with the initial CLI PR: 1. run_only mode is broken end-to-end. The daemon-side resolveTaskWorkspaceID() in internal/handler/daemon.go only resolves workspace from issue/chat, so run_only tasks (which have neither) return 404 from /start. BuildPrompt() would also emit an empty issue ID. The service-level resolver in internal/service/task.go already handles AutopilotRunID, but the daemon endpoint uses the handler copy. Fixing that path is out of scope for the CLI PR; drop run_only from the CLI and docs so we don't recommend a mode that cannot complete. Server continues to accept it for the existing UI. 2. UpdateAutopilot did not verify that a new assignee_id belongs to the workspace, unlike CreateAutopilot. This let a PATCH swap in an agent from a different workspace. Mirror the same GetAgentInWorkspace check. |
||
|
|
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> |
||
|
|
3d98f64ea1 |
Revert "fix(daemon): normalize hostname by stripping .local mDNS suffix (#1070)" (#1207)
This reverts commit
|
||
|
|
6428a10046 |
fix(daemon): normalize hostname by stripping .local mDNS suffix (#1070)
* fix(daemon): normalize hostname by stripping .local mDNS suffix Daemons started via different methods (standalone CLI vs desktop app bundled binary) resolve the hostname differently on macOS — one gets 'computer' and the other 'computer.local'. This caused duplicate runtime registrations for the same machine. Stripping the .local suffix at the point of hostname resolution ensures both always register under the same identifier. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(daemon): move empty-host fallback to after .local trim; fix Makefile @ prefix - Reorder: TrimSuffix runs first, then empty-check, so a hostname of just ".local" doesn't propagate as an empty daemon_id/device_name - Add missing @ prefix on migrate command in Makefile so it isn't echoed twice at startup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.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.
|
||
|
|
cd50c31201 |
feat(agent): add GitHub Copilot CLI backend (#1157)
* feat(agent): add GitHub Copilot CLI backend Integrate Copilot CLI as a new agent backend using the stable `-p` JSONL mode (`--output-format json`), following the same spawn-CLI-scan-JSONL pattern established by claude.go. Backend (server/pkg/agent/copilot.go): - Spawn `copilot -p <prompt> --output-format json --allow-all-tools --no-ask-user` - Parse streaming JSONL events (system/assistant/user/result/log) - Extract session ID for resume support (`--resume <id>`) - Accumulate per-model token usage for billing - Filter blocked args to prevent protocol-critical flag overrides Daemon config: - Probe MULTICA_COPILOT_PATH / MULTICA_COPILOT_MODEL env vars - Copilot uses AGENTS.md (native discovery) and default skills path Frontend: - Add Copilot logo SVG and provider switch case Tests: 14 unit tests covering arg building, event parsing, usage accumulation, and edge cases. All Go + TS checks pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(daemon): add restart subcommand, make daemon uses it - `daemon start` keeps original behavior: errors if already running - `daemon restart` stops existing daemon then starts fresh - `make daemon` now runs `daemon restart --profile local` Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(copilot): address review nits 1-5 - Nit 1: Add MinVersions["copilot"] = "1.0.0" - Nit 2: Seed activeModel from session.start.data.selectedModel (falls back to opts.Model, then "copilot"). First-turn tokens now get correct model attribution. - Nit 3: Handle assistant.reasoning/reasoning_delta → MessageThinking, reasoningText in assistant.message → MessageThinking, session.warning → MessageLog{warn} - Nit 4: Extract handleCopilotEvent() method shared by production and tests — no more duplicated switch body that can drift - Nit 5: Deltas write to output buffer as defense-in-depth; if process dies before assistant.message, output is non-empty Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
c0b4e7e8b8 |
feat(agent): add Cursor Agent CLI runtime support (#1057)
* feat(agent): add Cursor Agent CLI runtime support Add cursor-agent as a new agent backend, following the same pattern as existing providers. The implementation spawns cursor-agent CLI with stream-json output, parses JSONL events into the unified Message type, and supports session resume, usage tracking, and auto-approval (--yolo). Changes: - server/pkg/agent/cursor.go: cursorBackend implementation - server/pkg/agent/cursor_test.go: unit tests for args, parsing, errors - server/pkg/agent/agent.go: register "cursor" in New() factory - server/internal/daemon/config.go: probe cursor-agent in PATH - server/internal/daemon/execenv/context.go: cursor skill discovery path - server/internal/daemon/execenv/runtime_config.go: AGENTS.md injection - packages/views/.../provider-logo.tsx: cursor logo in UI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(agent): address PR review for cursor backend 1. Fix token usage double-counting: usage is now taken exclusively from "result" events (session totals). Per-message usage in "assistant" events is intentionally ignored. "step_finish" usage is only used as fallback when no "result" usage is available. 2. Remove dead code: isCursorUnknownSessionError() and its regex were defined but never called. Removed along with corresponding test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(agent): add missing CustomArgs, SystemPrompt, MaxTurns, and debug logging to cursor backend - Add cursorBlockedArgs and filterCustomArgs support for safe custom arg passthrough - Add --system-prompt and --max-turns flag support to buildCursorArgs - Add debug logging of command args before execution (consistent with all other backends) - Move stdout-close goroutine inside main goroutine (consistent with claude.go pattern) - Add tests for SystemPrompt/MaxTurns and CustomArgs filtering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: make daemon uses local profile & update Cursor logo to official brand - Makefile: make daemon now runs 'daemon start --profile local' for local dev - Replace Cursor runtime logo with official brand SVG (removed background rect) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(agent): remove unsupported --system-prompt and --max-turns from cursor-agent cursor-agent CLI does not support these flags. Instructions are already injected via AGENTS.md and .cursor/skills/ files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(agent): prevent step_finish + result usage double-counting in cursor Split usage accumulation into separate stepUsage and resultUsage maps. After stream ends, use resultUsage if available (session totals from result event), otherwise fall back to stepUsage (sum of step_finish). This prevents 2x counting when result.usage already includes totals. Added table-driven test covering: result-only, step_finish-only, step_finish+result (no double count), and multi-model scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(agent): fix misleading comment on cursor -p flag Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.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> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
8c518c350a |
feat(agent): add Pi agent runtime support (#1064)
* feat(agent): add Pi agent runtime support
Add Pi as a new agent runtime provider, following the established adapter
pattern. Pi CLI outputs JSONL events which are parsed for messages, tool
calls, and usage tracking.
Backend:
- New piBackend implementing the Backend interface (pi.go)
- Pi CLI discovery via MULTICA_PI_PATH env var or PATH lookup
- JSONL event stream parsing (agent_start, message_update, thinking_update,
tool_execution_start/end, agent_end)
- Usage scanner for ~/.pi/sessions/*.jsonl files
- Runtime config injection via AGENTS.md
- Skill injection to .pi/agent/skills/
Frontend:
- Pi provider logo (teal π icon)
- Pi label in transcript dialog
Docs:
- Updated all provider lists in README, CLI_INSTALL, and docs
* fix(agent): filter Pi usage scanner to agent_end events only
Address review feedback: restrict usage parsing to agent_end events
which contain cumulative totals, preventing potential inaccuracy if
Pi adds usage fields to other event types in the future.
* fix(agent): align Pi runtime with real CLI flags, event schema, and custom_args
- Flags: Pi's CLI uses `--mode json` (not `--output-format jsonl`), has no
`--yolo` (explicit `--tools` allowlist instead), takes the prompt as a
positional argument (not `-p <prompt>`), splits model as
`--provider <name> --model <id>`, and treats `--session` as a file path
that must exist before spawn.
- Event parsing: rewrite the stream event struct to match Pi's actual
JSON event schema (`message_update.assistantMessageEvent.delta`,
`turn_end.message.usage.{input,output,cacheRead,cacheWrite}`, etc.).
- Sessions: generate/persist session files under ~/.multica/pi-sessions/
and use the file path as the opaque SessionID returned to the daemon.
- Usage scanner: read assistant `message` events from the same session
files (Pi's session-file schema, distinct from the stdout stream).
- Custom args: consume `ExecOptions.CustomArgs` via `filterCustomArgs`
with a Pi-specific blocked set (`-p`, `--print`, `--mode`, `--session`)
so Pi matches the pattern shared by every other agent backend.
|
||
|
|
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 |
||
|
|
08c3513eef |
fix(cli): add pagination metadata to issue list JSON output and update agent prompt
Issue list JSON now includes total, limit, offset, has_more fields so agents can detect truncated results and paginate. Also documents --limit/--offset in the agent prompt and emphasizes mention format in Output section. Closes MUL-837 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
287a9eb546 |
fix(repocache): pass explicit env to remote-facing git subprocesses (#1029)
fix(repocache): pass explicit env to remote-facing git subprocesses |
||
|
|
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 |
||
|
|
9ba9ea66f8 |
fix(daemon): emphasize NEW comment in trigger prompt to prevent session confusion
When a comment-triggered task resumes an existing session, the agent may mistake the new comment for a previous one and skip it. Add [NEW COMMENT] tag to the prompt and reinforce in AGENTS.md workflow that the agent must respond to THIS specific comment, not prior ones. |
||
|
|
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> |
||
|
|
2cd6024851 |
Merge pull request #820 from zoharbabin/feat/local-storage-and-stdin
feat(cli): add --content-stdin flag to issue comment add |
||
|
|
bc1185f525 |
Merge pull request #755 from sanjay3290/feat/gemini-backend
feat(daemon): add Google Gemini CLI backend |
||
|
|
ff5f6ac2ee |
fix(daemon): prevent duplicate runtime registration on profile switch (#906)
* fix(daemon): prevent duplicate runtime registration on profile switch The daemon_id included a profile name suffix (e.g. "hostname-staging"), so switching profiles created a new daemon_id that bypassed the UPSERT dedup constraint, leaving orphaned runtime records in the database. Three changes: - Remove profile suffix from daemon_id — use stable hostname only. The unique constraint (workspace_id, daemon_id, provider) already prevents collisions within the same workspace. - Auto-migrate agents from old offline runtimes to the newly registered runtime during DaemonRegister (same workspace/provider/owner). - Add TTL-based GC in the runtime sweeper to delete offline runtimes with no active agents after 7 days. Closes MUL-695 * fix(daemon): address code review issues on PR #906 1. Move gcRuntimes() to the main sweep loop — previously it was inside sweepStaleRuntimes() after an early return, so it only ran when new runtimes were marked stale. Now it runs every sweep cycle independently. 2. Fix DeleteStaleOfflineRuntimes to exclude runtimes with ANY agent reference (not just active ones). The FK agent.runtime_id is ON DELETE RESTRICT, so archived agents also block deletion. 3. Scope MigrateAgentsToRuntime to the same machine by matching daemon_id LIKE '<current_daemon_id>-%'. This prevents cross-machine agent migration when the same user has multiple devices. |
||
|
|
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 |
||
|
|
6c92108b09 |
fix: replace hardcoded Unix path separators with filepath.Join and os.TempDir (#860)
- cmd_daemon.go: use filepath.Join for PID/log file paths instead of string concat with "/" - codex_home.go: use os.TempDir() instead of hardcoded "/tmp" for cross-platform fallback Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
526e336081 |
feat(execenv): add Windows fallback for symlink operations (#859)
On Windows, os.Symlink requires Developer Mode or admin privileges. Extract symlink creation into platform-specific files: on non-Windows, behavior is unchanged (os.Symlink). On Windows, try os.Symlink first, then fall back to directory junctions (mklink /J) for dirs and file copy for files. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
a73a9d4036 |
fix(agent): address PR review — env var blocklist, unmarshal logging, stable React keys
1. Security: add isBlockedEnvKey() blocklist that rejects MULTICA_* prefix and critical system vars (HOME, PATH, USER, SHELL, TERM, CODEX_HOME) from custom_env injection 2. Observability: log warnings when json.Unmarshal fails on custom_env (agentToResponse + claim endpoint) 3. UX: use stable auto-increment IDs for env entry React keys instead of array index to prevent input focus/state issues on add/remove |
||
|
|
4165401d16 |
feat(agent): support custom environment variables for router/proxy mode
Add per-agent custom_env configuration that gets injected into the agent subprocess at launch time. This enables users to configure custom API endpoints (ANTHROPIC_BASE_URL), API keys (ANTHROPIC_API_KEY), and cloud provider modes (CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX) without requiring code changes. Changes: - Migration 040: add custom_env JSONB column to agent table - Backend: custom_env in agent CRUD API + claim endpoint - Daemon: merge custom_env into subprocess environment variables - Frontend: env var editor in agent settings (key-value pairs with visibility toggle for sensitive values) Closes #816 Related: #807, #809 |
||
|
|
2f1000d815 |
merge: resolve conflict with main (runTask refactor + mergeUsage)
Main introduced executeAndDrain/mergeUsage refactor. Resolve by keeping main's refactored structure and re-applying EnvRoot to the switch/case in runTask. Rename newTestDaemon → newGCTestDaemon to avoid collision with the helper added in daemon_test.go on main. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
afab4dfdef |
Merge pull request #840 from multica-ai/agent/j/9cf0cf3e
fix(daemon): run repo cache sync in background to unblock heartbeat |
||
|
|
99e973ba3e |
fix(daemon): run repo cache sync in background to unblock heartbeat
The repoCache.Sync() call in loadWatchedWorkspaces runs synchronous git clone/fetch operations that can take minutes for large repos. Because heartbeatLoop and pollLoop only start after loadWatchedWorkspaces returns, the runtime's last_seen_at is never updated during the sync, causing the server's sweeper to mark it offline after 45 seconds. Move repo cache sync to a background goroutine so heartbeat and poll loops start immediately after runtime registration. Closes #825 |
||
|
|
14beaa6ce2 |
fix(daemon): extract pruneWorktree helper for idiomatic defer cancel
The context cancel in pruneRepoWorktrees was called explicitly after CombinedOutput inside a loop. Extract to a helper method so defer cancel() works correctly (scoped to the function, not the loop). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
20809052f5 |
fix(daemon): address GC review feedback
- Move WriteGCMeta from runTask() to handleTask() so it runs after task completion, not at start. Mid-task crashes leave orphan dirs that get cleaned by GCOrphanTTL. - Strengthen isBareRepo to check both HEAD and objects/ directory. - Remove empty workspace directories after all task dirs are cleaned. - Add 30s context timeout to git worktree prune to prevent hangs. - Add comprehensive unit tests for shouldCleanTaskDir (8 scenarios), cleanTaskDir, gcWorkspace empty-dir cleanup, isBareRepo, and WriteGCMeta/ReadGCMeta roundtrip. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
265d1854c9 |
fix(daemon): add fallback for failed session resume (#818)
* fix(daemon): add fallback for failed session resume When the daemon tries to resume a prior session (--resume flag for Claude, --session for OpenCode, session/resume RPC for Hermes) and the session no longer exists, the agent fails immediately. This adds a fallback that retries the execution with a fresh session instead of marking the task as blocked. Extracts the execute+drain logic into a reusable executeAndDrain method to avoid code duplication between the initial attempt and the retry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(daemon): narrow session resume retry and merge usage Address review feedback: 1. Narrow retry trigger: only retry when result.SessionID == "" (no session was established), not on any failure with PriorSessionID set 2. Merge token usage from both attempts so billing is accurate 3. Log errors when the retry itself fails to start 4. Add unit tests for mergeUsage, fallback behavior, and no-retry when session was already established Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
ff206baa6f |
feat(daemon): add periodic GC for workspace isolation directories
Isolation directories accumulate indefinitely because they're preserved
for session reuse but never cleaned up after the issue is closed.
This adds a background GC loop that periodically scans local workspace
directories and removes those whose issue is done/canceled and hasn't
been updated for 5 days (configurable via MULTICA_GC_TTL). Orphan
directories with no metadata are cleaned after 30 days.
Changes:
- Write .gc_meta.json (issue_id, workspace_id) at task completion
- Add GET /api/daemon/issues/{issueId}/gc-check endpoint for status queries
- Add gcLoop goroutine to daemon with configurable interval/TTL
- Prune stale git worktree references from bare repo caches each cycle
- New env vars: MULTICA_GC_ENABLED, MULTICA_GC_INTERVAL, MULTICA_GC_TTL,
MULTICA_GC_ORPHAN_TTL
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
c54f9a0bc4 |
Merge pull request #829 from multica-ai/agent/j/03e8009e
fix(daemon): embed triggering comment content in agent prompt |