mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-01 01:16:17 +02:00
agent/lambda/1f6f0a4f
246 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f675f03fbb |
fix(daemon/terminal): Phase 2 review — cleanup order, backpressure, origin (MUL-2295)
Address Phase 2 Round 1 review blockers + security item: 1. Fold daemonws teardown into a single cleanup defer so the order is: cancel heartbeat → wait hbDone → clearWSWrites → close(writes) → wait writer. The previous LIFO defer ordering let close(writes) run before the terminal bridge tore down, so an in-flight terminal pump could panic on send-to-closed-channel. 2. Replace silent drop of terminal.data on a saturated daemonws writer with real backpressure: pump uses sendWSFrameCtx (blocking with ctx escape) and bridge.closeAll now waits for every pump goroutine to exit before returning, giving the wakeup loop a hard barrier before close(writes). 3. terminalUpgrader now reuses realtime.CheckOrigin instead of CheckOrigin: true. The terminal endpoint executes shells; it must be at least as strict as the read-only realtime WS. Tests: - TestTerminalBridge_DataBackpressureNoSilentDrop pins that a full writes channel never loses bytes. - TestTerminalBridge_TeardownDoesNotPanicOnInFlightSend mirrors the wakeup defer sequence (closeAll → close(writes)) and asserts no panic. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0a97663acb |
feat(terminal): Phase 2 — daemonws routing + server proxy + Desktop xterm.js (MUL-2295)
Wires the Phase 1 terminal.Manager into the live daemonws transport so a
browser tab on the Multica Desktop app can attach to a PTY running in
the daemon's task workdir. End-to-end frame path:
Desktop (xterm.js) → /ws/issues/{id}/terminal (server proxy, cookie
or first-frame JWT auth, workspace membership
enforced before upgrade)
→ daemonws hub (new SendToRuntime + TerminalRouter routing
terminal.* frames back to the proxy by
request_id, then re-keyed on session_id)
→ daemon (new terminal_bridge.go owns one Manager per WS
connection, drains PtySession.Output() into
terminal.data frames, surfaces ExitC as
terminal.exit; closeAll on WS disconnect)
→ terminal.Manager.OpenWithInfo (new method) — server resolves
task.work_dir/issue_id/prior_session_id from its DB and embeds
them on the protocol; daemon trusts the server payload, no
daemon-local task cache needed.
Auth + ACL:
- Browser proxy enforces workspace membership before the upgrade.
- TerminalOpenPayload carries the resolved task info; cross-workspace
is structurally impossible at the bridge layer because both
OpenParams.WorkspaceID and TaskInfo.WorkspaceID come from the same
server-resolved field.
- terminal_bridge maps terminal.{Manager.OpenWithInfo} errors to the
protocol error codes (workspace_mismatch / task_not_found /
unsupported_os / spawn_failed / internal).
Resume:
- Server passes prior_session_id; daemon injects CLAUDE_SESSION_ID +
MULTICA_{WORKSPACE,ISSUE,TASK,USER}_ID into the PTY env per the RFC.
`claude --resume \$CLAUDE_SESSION_ID` continues the agent's session.
Lifecycle:
- Daemon installs a fresh terminalBridge per daemonws connection and
tears every PtySession down on disconnect; session_ids minted on one
WS cannot be reused on a reconnect because the server-side routing
registration is also gone.
- daemonws client.send buffer raised from 16 to 256 and read limit
from 4KB to 64KB so PTY traffic fits without evicting connections
used for heartbeat / wakeup hints.
Desktop UI:
- packages/views/issues/components/terminal-panel.tsx renders an
xterm.js console with FitAddon, base64 wire encoding, ResizeObserver
→ terminal.resize, reconnect button, and a clear web-only placeholder
when window.desktopAPI is absent.
- TerminalPanelSection wrapper hangs collapsed in the issue-detail
sidebar next to the execution log so bootstrap doesn't run for
every issue view.
Tests:
- terminal/manager_test.go: OpenWithInfo happy path + cross-workspace
reject (16 tests total, all -race clean).
- daemonws/terminal_test.go: TerminalRouter request_id → session_id
re-keying and unknown-session drop.
- daemon/terminal_bridge_test.go: server-supplied workdir round-trips
through OpenWithInfo, missing-workdir surfaces task_not_found and
never spawns, data+exit round trip.
- GOOS=windows go test -c clean for both daemon and daemon/terminal.
Phase 1 / 2 / 3 / 4 mapping unchanged: Phase 3 (CLI) and Phase 4
(execenv GC hook + issue runs entry + audit log) are still untouched.
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
953fdd5003 |
fix(daemon/terminal): close re-entry barrier + reap orphan PTY (MUL-2295)
- Manager.Close concurrent re-entry now blocks late callers on closeDone so every Close() return shares the "manager drained" guarantee. - Open cleanup path on lost race with Close calls pty.Wait() to reap the child synchronously (waitLoop never runs there). - Tests: concurrent Close callers all observe drained state; Open cleanup invokes pty.Wait at least once. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e70f44b92b |
fix(daemon/terminal): lock Done()/Manager.Close finalize order (MUL-2295)
Round 2 review fixes: 1. PtySession finalize sequence is now ExitC -> close(output) -> onClose/deregister -> close(done) so external waiters (bridge / GC hook / audit) can `<-Done()` and immediately query the manager without a race window. 2. Manager.Close now waits for each session's Done() (not just Close()) so by the time it returns the registry is empty and every session is fully finalized. Adds TestSession_DoneFiresAfterDeregister (locks the ordering contract) and TestManager_CloseWaitsForSessionFinalize (fakePTY.Wait delay proves Manager.Close blocks through finalize). Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
281f1073b5 |
fix(daemon/terminal): address Phase 1 review feedback (MUL-2295)
Wires in the four fixes Emacs flagged on the Phase 1 review: 1. Lifecycle: split stop/done with a WaitGroup. readLoop and idleLoop exit via <-stop; waitLoop is the finalizer that waits on the WG before closing output/done. Eliminates the "send on closed channel" race when the output buffer is saturated. Adds a regression test that fills output, calls Close, and verifies Done converges + ExitC fires before output closes (the doc contract). 2. Errors: Manager.Open wraps spawner errors with double-%w so errors.Is matches both ErrSpawnFailed and ErrUnsupportedOS. Adds a test with a fake spawner that returns ErrUnsupportedOS. 3. Close path on unix: SIGHUP to the process group, 250ms grace, SIGKILL, then close fd — comment now matches behavior. Skips the signal+sleep work entirely when the child already exited naturally. Manager.Close fans out per-session Close in parallel so the grace period doesn't multiply by session count. 4. IdleTimeout semantics: removes the NewManager default that silently rewrote 0 to 60min. Zero/negative now disables, per the doc comment. Added DefaultIdleTimeout for daemon wiring to opt in explicitly. Verified: go test, go test -race, GOOS=windows go test -c. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6758feba05 |
feat(daemon): add terminal Manager + PTY session (Phase 1, MUL-2295)
Daemon-side foundation for the Issue → Terminal feature. Manager owns
the lifecycle of all live PtySessions; sessions spawn a shell on a real
PTY via creack/pty (unix-only — Windows returns ErrUnsupportedOS until
ConPty support lands).
Open enforces the cross-workspace ACL — a client acting in workspace A
cannot attach to a task that belongs to workspace B. Each session
injects CLAUDE_SESSION_ID + MULTICA_{WORKSPACE,ISSUE,TASK,USER}_ID into
the child env so `claude --resume $CLAUDE_SESSION_ID` continues the
same session the agent run was using.
Adds the terminal.* WebSocket message types to server/pkg/protocol so
Phase 2 (daemonws routing) and Phase 3 (CLI) can land without touching
the manager.
Tests cover open, data round-trip, resize, explicit close, idle timeout
sweep, manager shutdown, cross-workspace rejection, and unknown task.
A fake Spawner backed by channels lets tests exercise lifecycle without
forking a real shell.
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
bfe9bf3eea |
feat(daemon): force-stop hung agent runs via idle watchdog (MUL-2281) (#2691)
* feat(daemon): force-stop hung agent runs via idle watchdog (MUL-2281) A backend whose subprocess hangs on a stuck child process (e.g. claude blocked on `docker ps` against a frozen dockerd) keeps the daemon's run record at status="running" until the full DefaultAgentTimeout (2 h) expires, because cmd.Wait() never returns and Session.Result is never written. MUL-2225 spent 17+ minutes in this state in the wild. Add a per-task idle watchdog around executeAndDrain: - Wrap the caller's ctx so a single cancel propagates to the agent subprocess (via the ctx passed to backend.Execute) AND the drain loop. - Stamp lastActivityAt every time the drain loop receives a message. - Tick at window/2; when idle_for >= window AND session.Messages buffer is empty, set a fired flag and call cancel. - Tag the resulting Result.Status as "idle_watchdog" so runTask routes it through a dedicated failure_reason instead of "agent_error". Default window is 5 min, configurable via MULTICA_AGENT_IDLE_WATCHDOG; set to 0 to disable. Tests cover the activity-then-silence case, the zero-message case, the disabled case, and the happy path. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): skip idle watchdog while a tool call is in flight A legitimate long-running tool call (npm install, docker build, test suite) can sit silent between tool_use and tool_result for many minutes. Without this gate, the watchdog would yank the agent mid-build. Track unmatched tool_use messages in an atomic counter; only let the watchdog fire when the counter is zero. tool_result clamps non-negative so a stray result with no matching use can't re-arm the watchdog one call too early. Adds two regression tests: - DoesNotFireDuringInFlightToolCall: tool_use -> silence past window -> tool_result -> completed (must NOT fire) - FiresAfterToolResultIfBackendStaysSilent: tool_use -> tool_result -> silence past window (MUST fire — backend really is stuck) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fcd13aece9 |
feat(daemon): auto-update CLI when idle (MUL-2100) (#2679)
* feat(daemon): auto-update CLI when idle (MUL-2100) Add a periodic poller that checks GitHub for a newer multica release every hour and self-updates when the daemon is idle, reusing the same brew-or-download upgrade path the Runtimes-page "Update" button already runs. - Refactor handleUpdate to call a shared runUpdate(target) helper so both server-triggered and auto-triggered upgrades go through the same brew detection + atomic replace + restart. - New autoUpdateLoop gates each tick on: opt-out flag, Desktop launch source, dev-build version, an in-flight update, and active tasks. The idle gate guarantees we never interrupt a running agent — busy ticks silently retry at the next interval. - Config: MULTICA_DAEMON_AUTO_UPDATE=false to disable (also via --no-auto-update), MULTICA_DAEMON_AUTO_UPDATE_INTERVAL to retune the poll period. - IsNewerVersion / IsReleaseVersion helpers in the cli package, with tests covering patch/minor/major bumps, dev-describe strings, and malformed input. - Daemon-side tests cover every skip path (updating, active tasks, fetch failure, no-newer) plus the success path that fires triggerRestart while keeping the updating flag held to the end. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): close idle race + verify checksum in auto-update (MUL-2100) Two issues raised in PR #2679 review: 1. The first idle check in tryAutoUpdate only ran before the release-metadata fetch, so a poller that won the claim race during the fetch could end up handing handleTask a task that triggerRestart was about to cancel via root- ctx cancellation. Add a strict claim barrier: runRuntimePoller now tryEnterClaim()s before ClaimTask, and tryAutoUpdate flips pauseClaims under claimMu only after observing claimsInFlight + activeTasks == 0. Pollers that were already mid-claim hold claimsInFlight > 0, so the barrier refuses to engage and the update defers to the next tick. 2. The direct-download path replaced the running binary with whatever bytes GitHub returned, without checking checksums.txt. Pull the manifest first, buffer the archive, and reject on SHA-256 mismatch before extraction. The GoReleaser config already publishes checksums.txt; we just consume it. Also tighten parseReleaseVersion so it stops accepting dev-describe shapes like "v0.1.13-5-gabcdef0" through the patch trim, matching its docstring. The auto-update loop already guards on IsReleaseVersion, but the lenient parser was a footgun and the existing test name even said "not newer" while asserting the opposite. Tests: - TestTryAutoUpdate_DefersWhenClaimInFlightAtBarrier (new race coverage) - TestTryAutoUpdate_HoldsBarrierAcrossRestart / ReleasesBarrierOnUpgradeFailure - TestTryEnterClaim_RespectsBarrier - TestFindChecksumManifestAsset / TestParseChecksumManifest / TestVerifyAssetSHA256 - TestIsNewerVersion: dev-describe cases now expect false (matches docstring) Co-authored-by: multica-agent <github@multica.ai> * chore(daemon): default auto-update poll interval to 6h (MUL-2100) 1h was overly chatty for a release that lands at most a few times a week. Operators who want a different cadence can still set MULTICA_DAEMON_AUTO_UPDATE_INTERVAL or --auto-update-interval. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b7a58c06ac |
Revert "feat(task): wire claim lease into TaskService and sweeper (MUL-2246) …" (#2673)
This reverts commit
|
||
|
|
bb32be0e50 |
feat(task): wire claim lease into TaskService and sweeper (MUL-2246) (#2662)
* feat(task): wire claim lease queries into TaskService and sweeper (MUL-2246) - ClaimTask now uses ClaimAgentTaskWithLease (generates claim_token + lease) - StartTask accepts optional claim_token for token-verified start - AgentTaskResponse includes claim_token for daemon to use - Daemon client sends claim_token in StartTask body - Sweeper calls RequeueExpiredClaimLeases each tick - Legacy daemons without claim_token still work (graceful fallback) Co-authored-by: multica-agent <github@multica.ai> * fix(task): address PR #2662 review blockers (MUL-2246) 1. ClaimAgentTaskForRuntime: push runtime_id into atomic SQL WHERE clause so runtime A cannot claim tasks queued for runtime B under the same agent. 2. Legacy StartAgentTask: add claim_token IS NULL guard so leased rows cannot be started without token verification. Handler rejects malformed tokens with 400 instead of silently degrading to legacy path. 3. StartAgentTaskWithClaimToken: validate claim_expires_at >= now(), preserve claim_token until terminal state (only clear claim_expires_at), use CTE + UNION ALL for idempotent retry when daemon resends after a lost StartTask response. Return 409 Conflict on token mismatch/expiry. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): StartTask 409 handling, transport retry, claim_token on FailTask (MUL-2246) - StartTask 409 (claim superseded): release slot, don't call FailTask - StartTask transport timeout/5xx: retry once with same token, then check task status before failing - FailTask now sends claim_token; server-side FailAgentTask SQL adds AND (claim_token IS NULL OR claim_token = @claim_token) guard so stale daemons cannot fail tasks that have been re-claimed Co-authored-by: multica-agent <github@multica.ai> * fix(task): close FailTask token bypass and RequeueExpiredClaimLeases liveness gap (MUL-2246) Blocker 1 - FailTask token validation: - SQL: change (param IS NULL OR claim_token = param) to (param IS NULL AND claim_token IS NULL) OR claim_token = param so tokenless requests can only fail legacy (tokenless) rows. - task.go: malformed claim_token now returns ErrInvalidClaimToken (400) instead of being silently dropped to NULL. - Handler: maps ErrInvalidClaimToken→400, ErrClaimTokenInvalid→409. - Service: when UPDATE returns no rows but task is still active, return ErrClaimTokenInvalid (token mismatch) instead of silent success. Blocker 2 - RequeueExpiredClaimLeases runtime liveness: - SQL: JOIN agent_runtime, only requeue tasks where runtime is 'online'. Dead/offline runtime tasks stay dispatched for FailTasksForOfflineRuntimes. - FOR UPDATE → FOR UPDATE OF atq (required with JOIN). Regression tests: - task_claim_token_test.go: malformed, tokenless-on-tokened, wrong-token - requeue_lease_test.go: SQL must JOIN agent_runtime with online filter Co-authored-by: multica-agent <github@multica.ai> * fix(task): move expired lease requeue to ClaimTaskForRuntime preflight, add heartbeat freshness backstop (MUL-2246) - Add RequeueExpiredClaimLeasesForRuntime: per-runtime preflight self-requeue in ClaimTaskForRuntime. Runtime proves liveness by actively claiming, so no heartbeat check needed. - Update global RequeueExpiredClaimLeases to require ar.last_seen_at freshness (stale_threshold_secs param). Prevents requeuing to a dead runtime in the 90s gap between lease expiry (60s) and offline detection (150s). - Add regression tests verifying the heartbeat freshness check and that the preflight query does not join agent_runtime. Co-authored-by: multica-agent <github@multica.ai> * fix(task): use LivenessStore for global requeue, move preflight before empty-cache (MUL-2246) Blocker 1: Global RequeueExpiredClaimLeases now uses LivenessStore.IsAliveBatch to verify runtimes are truly alive before requeuing expired leases. When LivenessStore is unavailable (no Redis), global requeue is skipped entirely — the preflight self-requeue in ClaimTaskForRuntime handles live runtimes. This closes the 60-150s gap where a dead runtime still appears online in DB. Blocker 2: Moved RequeueExpiredClaimLeasesForRuntime BEFORE EmptyClaim.IsEmpty fast-path in ClaimTaskForRuntime. Expired leases are now requeued (which bumps the empty cache via notifyTaskAvailable) before the empty check can short-circuit the claim path. Also adds ListRuntimesWithExpiredClaimLeases SQL query and LivenessChecker interface on TaskService. Co-authored-by: multica-agent <github@multica.ai> * fix(task): wire EmptyClaimCache into backend taskSvc for backstop requeue (MUL-2246) The backend taskSvc used by the sweeper only had Liveness wired but not EmptyClaim. When global backstop requeue called notifyTaskAvailable, s.EmptyClaim.Bump() was a nil no-op — the handler's empty-cache was never invalidated, so the daemon's next claim hit a stale empty verdict. Fix: wire the same Redis-backed EmptyClaimCache into the backend taskSvc in main.go (same Redis keys as router.go:139 handler instance). Add regression test verifying backstop requeue invalidates the handler's empty-cache. Co-authored-by: multica-agent <github@multica.ai> * fix(task): global backstop must not requeue — alive runtimes use preflight, dead stay dispatched (MUL-2246) - RequeueExpiredClaimLeases is now a no-op (returns 0 always) - Alive runtimes self-requeue via ClaimTaskForRuntime preflight - Dead runtimes stay dispatched for FailTasksForOfflineRuntimes - Rewriting to queued on dead runtime creates 2h blackhole (offline sweeper only handles dispatched/running) - Test actually calls RequeueExpiredClaimLeases and asserts 0 in all cases Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): remove duplicate usage reporting block after merge conflict (MUL-2246) The merge resolution introduced a second ReportTaskUsage call after the status check, duplicating the usage-before-early-return block that already runs right after runner.run. Remove the duplicate and add a regression test asserting /usage is called exactly once on the normal completion path. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4c1fd60215 |
fix(daemon): report task usage before cancel check (#1180)
handleTask had two early-return paths that ran before ReportTaskUsage: the cancelledByPoll select and the post-run GetTaskStatus check. Both silently discarded any usage accumulated by the agent — and both claude.go and codex.go populate Result.Usage even when runCtx is cancelled mid-run, so cancelled tasks consistently under-reported tokens. Hoist ReportTaskUsage to run immediately after the runner returns, before any early-return path. Add a taskRunner interface seam and a cancelPollInterval field so tests can inject a fake runner and trigger the poll-cancellation path on a 10ms ticker without spawning real agents. Two regression tests cover both leak windows: - TestHandleTask_ReportsUsageBeforeCancel: post-run /status returns "cancelled"; usage must be reported before the status check. - TestHandleTask_ReportsUsageWhenCancelledByPoll: poll goroutine fires first and cancels runCtx; runner returns usage on Done; assert poll-status precedes usage (proving the cancelledByPoll branch was the one exercised, not the post-run path). Sanity-checked: reverting only the ReportTaskUsage hoist fails both tests with the original "tokens lost" message. MUL-2258 Co-authored-by: Jiang Bohan <bhjiang@outlook.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
da7b33561e |
fix: make quick-create output prefix agnostic (#2604)
* fix: make quick-create output prefix agnostic * fix: remove quick-create prefix assumption from runtime config |
||
|
|
464201ba0d |
feat(execenv): native OpenClaw skill discovery via per-task config (MUL-2219) (#2628)
* feat(execenv): native OpenClaw skill discovery via per-task config
MUL-2213 stopped lying about native discovery and routed openclaw skills
to .agent_context/skills/ — a path openclaw's scanner never reads.
Multica skills attached to openclaw-backed agents were still invisible to
the runtime; the AGENTS.md fallback was only a documentation patch.
OpenClaw's skill scanner walks <workspaceDir>/skills/ (plus a few other
roots), and workspaceDir is resolved from the openclaw config file —
specifically agents.list[id].workspace → agents.defaults.workspace →
~/.openclaw/workspace. There is no CLI flag or env var override on the
agent runtime; the only knob is the config file.
This change wires a per-task synthesized config:
1. execenv.prepareOpenclawConfig deep-copies the user's existing
openclaw.json (priority: $OPENCLAW_CONFIG_PATH, else
~/.openclaw/openclaw.json), rewrites agents.defaults.workspace AND
every agents.list[].workspace to the task workdir, and writes the
result to {envRoot}/openclaw-config.json. Provider sections,
registered agents, model providers, gateway settings — everything
openclaw needs to actually start — are preserved as-is.
2. resolveSkillsDir for "openclaw" now points at {workDir}/skills/,
which is the first path openclaw scans under workspaceDir. Skills
written here are picked up natively.
3. daemon.go exports OPENCLAW_CONFIG_PATH={env.OpenclawConfigPath} on
the openclaw subprocess and adds OPENCLAW_CONFIG_PATH to the
custom_env blocklist so users cannot accidentally override it.
4. buildMetaSkillContent now lists openclaw alongside the
"discovered automatically" providers; the .agent_context/skills/
fallback line stays for gemini/hermes.
The new regression test TestPrepareOpenclawSkillWriteMatchesScanPath is
the one MUL-2219's DoD calls out: it resolves the workspaceDir the way
openclaw does (reading agents.defaults.workspace out of the synthesized
config) and proves {workspaceDir}/skills/<name>/SKILL.md is what Multica
actually wrote. The pre-MUL-2219 fix asserted "we wrote a file" without
checking the scanner would ever see it — which is how the dead drop into
.openclaw/skills/ landed in #2621's first commit.
Verified locally: minimum-viable synthesized config validates via
`openclaw config validate`, and `OPENCLAW_CONFIG_PATH=<path> openclaw
config get agents.defaults.workspace` returns the task workdir as
expected. MUL-2219
Co-authored-by: multica-agent <github@multica.ai>
* fix(execenv): delegate openclaw config parsing to CLI and fail closed
Address Elon's must-fix on PR #2628: the previous implementation parsed
~/.openclaw/openclaw.json with encoding/json, which cannot read JSON5
or follow $include — the OpenClaw spec's actual format. When parsing
failed, prepareOpenclawConfig silently emitted a minimal config, which
could boot OpenClaw without the user's registered agents, model
providers, or API keys.
Two changes:
1. Delegate active-config-path resolution and config reading to the
openclaw CLI itself. `openclaw config file` locates the active
config (covering OPENCLAW_CONFIG_PATH / OPENCLAW_STATE_DIR /
OPENCLAW_HOME / default and the legacy chain), and the wrapper we
write uses $include to point at it so OpenClaw's own loader handles
JSON5, $include nesting, env-substitution, and secret refs. We read
only agents.list via `openclaw config get --json` to rewrite each
entry's workspace — secrets, comments, and includes in the user
config are never touched.
2. Remove the silent minimal-config fallback. Any CLI failure,
malformed output, or write error now surfaces as a hard error from
Prepare / Reuse. The only "synthesize minimal" path left is a fresh
install (CLI reports a path but the file doesn't exist), where
there is no user data to lose.
The per-task override still rewrites every agents.list[].workspace,
not just agents.defaults.workspace — this is intentional task
isolation, documented in prepareOpenclawConfig and the PR body. A
host-scope per-agent workspace would otherwise silently route the
scanner back to the user's shared workspace.
Cleanups Elon flagged in the same review:
- daemon.go inline-system-prompt comment no longer claims openclaw
ignores the task workdir; it does load it now, and the inline brief
is a belt-and-suspenders carryover for older releases.
- execenv.go openclaw block no longer references "skill file paths in
the inline brief" — the brief uses "discovered automatically".
Reuse() switches to a ReuseParams struct so the openclaw binary path
threads through alongside CodexVersion without a 6th positional arg.
MUL-2219
Co-authored-by: multica-agent <github@multica.ai>
* fix(execenv): grant OpenClaw $include cross-dir confinement for per-task wrapper
The per-task wrapper at envRoot/openclaw-config.json $includes the user's
active config (typically ~/.openclaw/openclaw.json), but OpenClaw confines
$include resolution to the wrapper file's directory unless the target's
parent is granted via OPENCLAW_INCLUDE_ROOTS. Without this, OpenClaw refuses
to follow the link at runtime and the wrapper boots with no user-registered
agents.
prepareOpenclawConfig now returns dirname(activePath) as IncludeRoot, and
the daemon prepends it to whatever the user already has in
OPENCLAW_INCLUDE_ROOTS via the new composeOpenclawIncludeRoots helper
(dedupes, drops empty segments, preserves user-configured roots). Fresh
install emits no $include and leaves the env var untouched.
Adds OPENCLAW_INCLUDE_ROOTS to the custom_env blocklist so a per-agent
override cannot strip the granted root.
Regression tests:
- TestPrepareOpenclawConfigWrapperLoadableUnderIncludeConfinement asserts
every $include target's dirname is covered by the IncludeRoot we surface.
- TestPrepareEnvironmentOpenclawWiresIncludeRoot covers the non-fresh-install
Environment wiring.
- TestComposeOpenclawIncludeRoots covers the daemon-side env composition
(preserve, dedupe, drop empties).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
8572a79950 |
MUL-2215: fix(daemon): close handleRuntimeGone success/straggler race (#2623)
* MUL-2215: fix(daemon): close handleRuntimeGone success/straggler race handleRuntimeGone coalesced concurrent recoveries with a per-workspace `reregisterNextAttempt` slot that was deleted immediately on success. A late-arriving goroutine whose `removeStaleRuntime` was delayed by mutex contention could reach the coalesce gate after the winner cleared the slot, observe no slot, re-claim, and double-register — the source of the intermittent `register endpoint called 2 times under stampede, want 1` failure on PR #2348. The slot delete on success is intentional (a genuinely later distinct deletion in the same workspace must register again, validated by TestHandleRuntimeGone_DistinctDeletionsWithinCoalesceWindowBothRecover), so we can't just extend the slot's lifetime. Add a second per-workspace gate: `reregisterLastCompletedAt`. Every call captures `entryAt` at the top of handleRuntimeGone; at the coalesce gate a caller bails if `lastCompletedAt >= entryAt`, i.e. a peer's register completed AFTER we entered the function. Same-wave stragglers bail deterministically; distinct later events have `entryAt > lastCompletedAt` and proceed. Extracted the gate into `tryClaimRegisterSlot` / `recordRegisterCompletion` so the race can be exercised deterministically with synthetic timestamps instead of relying on `-count=N` to win the scheduling lottery. - TestHandleRuntimeGone_CoalescesConcurrentCallers: -count=500 -race clean (previously intermittent). - New unit tests cover the straggler bail, the distinct-later-event claim, failure backoff suppression, and peer-holds-slot coalescing. Co-authored-by: multica-agent <github@multica.ai> * MUL-2215: narrow completion stamp to success path Second review caught that recordRegisterCompletion stamped lastCompletedAt on both success and failure. A failed register has not covered any workspace state, so a same-wave straggler whose entryAt predates the failure must be allowed to retry once the failure backoff expires — the previous behavior would let the failure-time stamp also hide that straggler. workspaceSyncLoop only retries when a workspace's runtimeIDs fully drain, so partial-deletion recovery has to come from the straggler path. Failure path now only updates reregisterNextAttempt; success path keeps its existing stamp + slot clear. Add a regression test covering the entryAt-before-failed-completion / arrival-past-backoff edge. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f82a6adde9 |
fix(execenv): fall back OpenClaw skills to .agent_context/skills/ and stop claiming native auto-discovery (#2621)
* fix(execenv): write OpenClaw skills to .openclaw/skills/ for native discovery The OpenClaw provider was missing a case in resolveSkillsDir, so workspace skills attached to OpenClaw-backed agents fell through to .agent_context/ skills/ — a path the openclaw CLI never inspects. The result: agents created against the OpenClaw runtime saw zero of their loaded Skills in chat or task runs, even though the meta AGENTS.md content advertised them as auto-discovered. Mirrors the same per-provider mapping already in place for OpenCode, Copilot, Pi, Cursor, Kimi, Kiro. Also adds .openclaw to the repocache git-exclude list so the per-task skills directory does not pollute checked-out repos. MUL-2213 Co-authored-by: multica-agent <github@multica.ai> * fix(execenv): drop .openclaw/skills dead-drop write; flag openclaw as non-auto-discovery Reviewer (Elon) pointed out that {workDir}/.openclaw/skills/ is not in any OpenClaw skill discovery path. Confirmed by reading openclaw upstream (src/agents/skills/refresh.ts, src/agents/agent-scope-config.ts, src/cli/program/register.agent.ts): - OpenClaw scans <workspaceDir>/skills, <workspaceDir>/.agents/skills, ~/.openclaw/skills, ~/.agents/skills, bundled, and config skills.load.extraDirs. - workspaceDir is resolved from the openclaw config (per-agent workspace -> agents.defaults.workspace -> ~/.openclaw/workspace). It is NOT the cwd of the openclaw process. - There is no --workspace CLI flag on 'openclaw agent', and no OPENCLAW_WORKSPACE env var consumed at runtime. The only knob is the config file. So {workDir}/.openclaw/skills/ written by Multica is never seen by the openclaw runtime, and the meta AGENTS.md was lying to the agent by claiming auto-discovery. Reverts: - resolveSkillsDir: drop the openclaw case; falls back to .agent_context/skills/ (same path as hermes). - agentGitExcludePatterns: drop .openclaw; nothing is written there now. Also updates the openclaw branch in buildMetaSkillContent to point the agent at .agent_context/skills/ explicitly (alongside gemini/hermes), so loaded skills are at least referenced by path in the AGENTS.md context. The openclaw native loader still won't see them as installed skills. Native auto-discovery for openclaw needs per-task workspace integration (e.g. synthesized per-task config via OPENCLAW_CONFIG_PATH that overrides agents.defaults.workspace, or resolving the agent's actual configured workspace at exec time) — tracked as follow-up. MUL-2213 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
08e355be0b |
MUL-2167: fix(daemon): resolve agent CLIs via login shell when daemon PATH misses them (#2620)
* fix(daemon): resolve agent CLIs via login shell when daemon PATH misses them GUI-launched daemons on macOS/Linux do not inherit the user's interactive shell PATH, so fnm/nvm/volta multishells and the Anthropic native installer silently disappear during onboarding even though `claude --version` works in Terminal. Fall back to `$SHELL -ilc` to ask the login shell for the canonical absolute path, then verify it with exec.LookPath before trusting it. Symlinks (fnm/nvm prefix dirs) are resolved while the helper shell is still alive so per-session paths get canonicalised before they vanish. Refs MUL-2167, multica-ai/multica#2512. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): strip alias shadowing, harden timeout, lazy-resolve via login shell Three follow-ups from the PR #2620 review (Elon): 1. Alias shadowing — `command -v claude` in zsh/bash returns the alias definition, not the binary, and the absolute-path filter then rejects it. The script now `unalias`/`unset -f` the name before lookup so `command -v` falls through to the real PATH binary. This is the exact case behind #2512. 2. Hard timeout — `CommandContext` kills only the shell process. Rc files that background processes inheriting stdout (`direnv hook`, `nvm` shims, plain `&`) keep the pipe open and `cmd.Output()` would block for as long as the survivors live. `Cmd.WaitDelay` forcibly closes the pipes once the cap elapses, so total startup penalty is bounded by `timeout + waitDelay` regardless of rc-file content. 3. Lazy fallback — the resolver no longer runs on every daemon start. `getShellResolved` is `sync.Once`-guarded and only fires when a bare command name actually misses `exec.LookPath`. Users whose PATH already contains every agent never pay the rc-file load cost. Tests: - `TestResolveAgentsViaLoginShell_StripsAliasShadowing` — rc declares `alias fakeclaude=...`, real binary lives on PATH, resolver must return the binary, not the alias text. - `TestResolveAgentsViaLoginShell_HardTimeoutOnBackgroundedStdout` — rc backgrounds a 60s sleeper holding stdout; resolver must return inside `timeout + waitDelay + slack`, not 60s. - `TestLoadConfig_SkipsLoginShellWhenLookPathSucceeds` — when exec.LookPath finds every agent, SHELL (a marker-writing sentinel) must not be invoked. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fdf19cac8f |
fix(quick-create): default squad-picked issues to the squad, not the leader (#2611)
When the user opens quick-create with a squad selected, the task is enqueued against the squad's leader agent — but the squad, not the leader, is the expected owner. The prompt previously instructed the leader to "default to YOURSELF" using its own agent UUID, hiding new issues from the squad's delegation flow. Surface the squad's id + name on the claim response and branch the default-assignee instruction in buildQuickCreatePrompt: when SquadID is present, point --assignee-id at the squad UUID and explicitly forbid self-assignment. MUL-2203 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
58cc189dcd |
fix: honor quick-create squad mentions (#2586)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
376cc8372a |
fix: inject squad leader no_action rule for member-triggered comments (#2576)
The per-turn prompt in buildCommentPrompt() only injected the squad leader no_action prohibition inside the 'if TriggerAuthorType == agent' block. When a member (human) posted a comment like 'LGTM', the squad leader was triggered but the per-turn prompt did NOT include the prohibition, causing the model to post noise comments like 'LGTM is a pure acknowledgment — no reply needed. Exiting silently.' Fix: move the squad leader no_action rule outside the agent-only block so it fires for ALL trigger types (agent and member). Fixes: MUL-2168 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f6ac53a967 |
fix: squad leader no_action must not post comment on comment-triggered path (#2573)
PR #2564 only added IsSquadLeader handling to the assignment-triggered workflow path and the Output section. When a squad leader is triggered by a comment (the common case for re-evaluation), the comment-triggered workflow path had NO squad leader special handling, so the model still posted comments announcing no_action/silence. Changes: - runtime_config.go: Add IsSquadLeader check to comment-triggered step 4 with explicit prohibition against posting no_action announcement comments - runtime_config.go: Strengthen Output section from 'may exit silently' to 'MUST exit without posting any comment' with explicit DO NOT examples - runtime_config.go: Strengthen assignment-triggered step 5 similarly - prompt.go: Add squad leader no_action rule to per-turn comment prompt when trigger author is an agent and agent instructions contain the Squad Operating Protocol marker - Add tests for both the per-turn prompt and CLAUDE.md generation Fixes MUL-2168 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cc9fbd3db0 |
Fix stale Done replies on comment follow-ups (#2495)
* fix: avoid stale done replies on comment follow-ups * fix: avoid inlining runtime brief for Hermes ACP * fix: address comment follow-up review feedback |
||
|
|
7a1284128d |
fix: allow squad leader to exit silently on no_action without posting a comment (#2564)
The runtime prompt's Output section unconditionally required all tasks to post a comment via 'multica issue comment add', which conflicted with the squad leader protocol that says to 'exit silently' on no_action. Changes: - Add IsSquadLeader bool to TaskContextForEnv (detected via Squad Operating Protocol marker in agent instructions) - Relax the Output section and assignment-triggered workflow step 5 to allow squad leaders to exit with only a 'multica squad activity' call when the outcome is no_action Fixes MUL-2168 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
21b49eb59b |
fix(cli): resolve squad assignees in issue create/update/assign (MUL-2165) (#2551)
* fix(cli): resolve squad assignees in issue create/update/assign (MUL-2165) The CLI assignee resolver only searched workspace members and agents, so a quick-create input like "assign to <SquadName>" silently fell through to "Unrecognized assignee: <SquadName>" in the issue description — even though squads are first-class assignees server-side and the prompt's whole point was to route the work for the user. Extend resolveAssignee / resolveAssigneeByID to also fetch /api/squads, teach the actor display lookup to render squad names in table output, update the quick-create prompt and runtime-config command listing to mention `multica squad list` alongside members and agents, and lock in the new behavior with tests. Co-authored-by: multica-agent <github@multica.ai> * fix(cli): gate squad assignee resolution behind an allowed-kinds set (MUL-2165) The earlier MUL-2165 fix taught resolveAssignee / resolveAssigneeByID to also return (squad, ...), but those helpers are shared. Project lead and issue subscriber callers were still using them, and their target schemas reject squads — project.lead_type has a DB CHECK constraint (server/migrations/034_projects.up.sql:10) and the subscriber handler's isWorkspaceEntity switch only knows member/agent (server/internal/handler/handler.go:414). So `multica project create --lead "<SquadName>"` and `multica issue subscriber add --user "<SquadName>"` would resolve to (squad, ...) and surface as a 500/403 server-side instead of a clean CLI-side resolution error. Thread an assigneeKinds set through the resolver and the pickAssigneeFromFlags helper. Issue create/update/assign/list pass `issueAssigneeKinds` (all three); project lead and subscriber pass `memberOrAgentKinds`. The squads fetch is skipped entirely when not allowed, and the not-found / no-match error wording adapts to the allowed kinds so it never mentions a type the caller cannot use. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
384ddcbe65 |
fix(execenv): seed user-installed Codex skills into per-task CODEX_HOME MUL-1626 (#2519)
* fix(execenv): seed user-installed Codex skills into per-task CODEX_HOME Codex is the only daemon runtime whose HOME is redirected — the daemon sets CODEX_HOME to a per-task isolated directory so each task gets a clean config slate without polluting ~/.codex/. Side effect: the codex CLI never sees the user's `~/.codex/skills/` and tells the user no skill was found. Other runtimes (claude / copilot / opencode / pi / cursor / kimi / kiro) don't have this issue: they leave HOME untouched and discover both user-level skills (from ~/.<runtime>/skills) and workspace-assigned skills (written to a workdir-local dotfile dir) natively. Codex is the outlier. Fix: in execenv.Prepare and execenv.Reuse, copy each subdirectory under `~/.codex/skills/` into the per-task `codex-home/skills/` before writing workspace-assigned skills. Workspace skills still win on sanitized-name conflict; user-level installer symlinks (lark-cli style) are followed so the per-task home gets real content rather than dangling links. Closes #1922 Co-authored-by: multica-agent <github@multica.ai> * fix(execenv): wipe per-task codex skills dir before each hydration Without this, the Reuse path leaves two classes of stale state behind: 1. Round 1 seeded user skill `writing/drafts/stale.md`. Round 2 reuses the same workdir with workspace skill `Writing` assigned: seed stage skips user `writing` (reserved), workspace stage writes `SKILL.md` via MkdirAll + WriteFile but never clears the directory, so the round-1 user support files surface under the workspace skill — violating "workspace fully wins on name conflict" and potentially leaking user-level files into a workspace skill view. 2. User uninstalls a skill from ~/.codex/skills between two runs. The prior copy in codex-home/skills/<name>/ lingers, so the codex CLI keeps seeing the removed skill. Fix: RemoveAll(codex-home/skills) at the start of hydrateCodexSkills, then re-seed user skills and re-write workspace skills. On Prepare this is a no-op (envRoot was already wiped); on Reuse it resets the slate. Added two regression tests covering both scenarios. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ff27142b69 |
fix: treat empty output on successful completion as completed, not blocked (#2507)
When an agent completes successfully (exit 0) but produces no text output, the daemon incorrectly classified it as 'blocked'. This is wrong — agents can legitimately complete work via tool calls (posting comments, pushing code) without emitting text output. Change the empty-output path to return status=completed so the task is correctly reported as successful. Fixes MUL-2104 Co-authored-by: yushen <ldnvnbl@gmail.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
86aa5199fc |
feat(chat): support attachments & images in chat input (#2445)
* docs(plans): chat attachment & image support implementation plan Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(db): add chat_session_id/chat_message_id to attachment Co-authored-by: multica-agent <github@multica.ai> * feat(db): sqlc — chat_session_id on CreateAttachment + LinkAttachmentsToChatMessage Co-authored-by: multica-agent <github@multica.ai> * feat(file): upload-file accepts chat_session_id form field Co-authored-by: multica-agent <github@multica.ai> * feat(chat): SendChatMessage links uploaded attachments to the new message Co-authored-by: multica-agent <github@multica.ai> * feat(api): uploadFile accepts chatSessionId; sendChatMessage accepts attachmentIds Co-authored-by: multica-agent <github@multica.ai> * feat(core): useFileUpload supports chatSessionId context Co-authored-by: multica-agent <github@multica.ai> * feat(chat): support paste/drag/upload attachments in chat input Co-authored-by: multica-agent <github@multica.ai> * test(e2e): chat input attachment upload + send round-trip Co-authored-by: multica-agent <github@multica.ai> * chore(chat): keep lazy-created session title empty so untitled fallback localizes Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address review — dedupe ensureSession + parse upload response - chat-window: cache in-flight createSession promise in a ref so a file drop followed by a quick send no longer spawns two sessions (and orphans the attachment on the losing one). - Attachment type + EMPTY_ATTACHMENT + AttachmentResponseSchema: include the new chat_session_id / chat_message_id fields the server now returns. - uploadFile: route the response through parseWithFallback so a malformed body returns EMPTY_ATTACHMENT instead of an undefined-keyed Attachment, matching the API boundary rule. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address PR #2445 review — test ctx, send gating, attachment surface 1. Backend test was 400ing because the handler reads workspace from middleware-injected ctx, and `newRequest` only sets the header. Helper `withChatTestWorkspaceCtx` mirrors the agent-access-test pattern and loads the member row + SetMemberContext before invoking the handler. 2. Attachment metadata now flows end-to-end: - new sqlc `ListAttachmentsByChatMessageIDs` (batch lookup, mirrors the comment-side query) - `chatMessageToResponse` takes `attachments` and `ChatMessageResponse` surfaces them — same shape as CommentResponse - `ListChatMessages` loads them via a new `groupChatMessageAttachments` helper so the chat bubble can render file cards - daemon claim path pulls `ListAttachmentsByChatMessage` for the latest user message and ships `ChatMessageAttachments` to the daemon - `buildChatPrompt` lists id+filename+content_type and instructs the agent to `multica attachment download <id>` — fixes the private-CDN expiring-URL problem where the markdown URL would have expired by the time the agent acts - TS `ChatMessage` gains an optional `attachments` field 3. Chat composer now blocks send while uploads are in flight: - `pendingUploads` counter increments in handleUpload, SubmitButton uses it to disable - handleSend also gates on `editorRef.current.hasActiveUploads()` to catch the Mod+Enter path that bypasses the button - new vitest covers the "drop large file → immediate send" scenario where attachment id would otherwise be silently dropped Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * chore: drop implementation plan doc Process artefact, not something the repo needs to keep. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
046e4b1efa |
fix(execenv): switch every provider's Windows reply template to --content-file (#2411)
Three user reports converge on the same Windows-shell encoding bug: - #2198 / #2236 — Chinese, Codex on Win11. Comments / descriptions generated by the agent arrive as `?`. - #2376 — Cyrillic, non-Codex agent ("Ops Lead") on Win11 Desktop. Title preserved (argv → CreateProcessW UTF-16), description / agent reply garbled (stdin → shell-codepage re-encoding). woodcoal's independent diagnosis on #2198 confirms the root cause: Windows PowerShell 5.1's `$OutputEncoding` defaults to ASCIIEncoding when piping to a native command, so non-ASCII bytes are silently replaced with `?` before they reach `multica.exe`. The CLI's stdin parsing is fine; the bytes are corrupted upstream, in the agent's shell layer. This PR ships the fix that supersedes the codex-only attempt in PR #2265 (which is closed in favour of this one): ## CLI Add `--content-file <path>` to `multica issue comment add` and `--description-file <path>` to `multica issue {create,update}`. The CLI reads bytes off disk via `os.ReadFile` and skips the shell entirely; UTF-8 survives end-to-end regardless of `$OutputEncoding` or `chcp`. The three input modes (`--content`, `--content-stdin`, `--content-file`) are mutually exclusive. ## Runtime config `buildMetaSkillContent`'s Available Commands section is rewritten as a neutral three-mode menu. The previous unconditional "MUST pipe via stdin" / `--description-stdin` mandate (over-spread from #1795 / #1851's Codex-multi-line fix) is gone for non-Codex providers; the strong directive now lives only in the Codex-Specific section, which branches on host: - Codex / Linux+macOS: `--content-stdin` + HEREDOC (preserves MUL-1467 fix against codex's literal `\n` habit). - Codex / Windows: `--content-file` (PowerShell ASCII pipe is the exact bug we're patching). ## Per-turn reply template `BuildCommentReplyInstructions` now takes a provider arg and branches provider × OS: - Windows + any provider → `--content-file` (the bug is shell-layer, not provider-layer; #2376 shows non-Codex agents on Windows also hit it). All providers write a UTF-8 file with their file-write tool and post via `--content-file ./reply.md`. - Linux/macOS + Codex → stdin/HEREDOC (MUL-1467 protection). - Linux/macOS + non-Codex → lightweight pre-#1795 inline `--content "..."`. The CLI server-side decodes `\n`, so escaped multi-line works; the agent retains stdin / file as escape hatches for richer formatting. `BuildPrompt` and `buildCommentPrompt` gain a `provider` arg; `daemon.runTask` already has it in scope. ## Tests - `TestResolveTextFlag` — file-source verbatim with non-ASCII (`标题 / Заголовок / 中文段落`), missing-file error, empty-file rejection, three-way mutual exclusion. - `TestInjectRuntimeConfigAvailableCommandsIsNeutral` — every non-Codex provider × {linux, darwin, windows} pins the three-mode menu present + over-spread "MUST stdin" substrings absent. - `TestInjectRuntimeConfigCodexLinuxEmphasizesStdin` + `TestInjectRuntimeConfigCodexWindowsUsesContentFile` — Codex section's per-OS branch. - `TestBuildCommentReplyInstructionsCodexLinux` + `TestBuildCommentReplyInstructionsNonCodexLinux` + `TestBuildCommentReplyInstructionsWindowsUsesContentFile` — the reply-template provider × OS matrix. - `TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin` — end-to-end AGENTS.md / CLAUDE.md on Windows has no prescriptive stdin directive, for claude / codex / opencode. `go test ./...` and `go vet ./...` clean. Closes #2198, #2236, #2376. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fae8558263 |
fix(daemon): self-heal when a runtime is deleted server-side (#2404)
Closes #2391. |
||
|
|
fb026f2607 |
fix(daemon): suppress git console windows on Windows (#2358)
* fix(daemon): suppress git console windows on Windows Apply the same HideConsoleWindow pattern used for agent processes (PR #1474) to all git commands spawned by the daemon's repo-cache, execenv, and GC packages. Each exec.Command now calls util.HideConsoleWindow(cmd) which sets CREATE_NEW_CONSOLE + HideWindow so grandchildren inherit a hidden console instead of flashing visible console windows. Closes #2357 Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com> * refactor: use EnsureHiddenConsole at daemon startup Replace per-site HideConsoleWindow(cmd) calls with a single EnsureHiddenConsole() invoked once at daemon startup. The daemon now owns a hidden console that every child process (git, cmd /c mklink, etc.) inherits automatically, eliminating the need for per-call SysProcAttr configuration. This also covers the previously missed exec.Command in codex_home_link_windows.go (cmd /c mklink) which never had a HideConsoleWindow call. Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com> --------- Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com> Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com> |
||
|
|
d6349c16ec |
feat(runtime): per-runtime timezone for token-usage aggregation (MUL-1950) (#2394)
* feat: per-runtime timezone for token usage aggregation The runtime token-usage charts (daily and hourly tabs on the runtime-detail page) bucketed every event by the Postgres session timezone, which is UTC in production. For an operator in UTC+8 that meant a Tuesday afternoon's tasks landed in Tuesday early-morning's bar — the chart was always one off. Fix: store an IANA timezone on agent_runtime and aggregate under it. * migrations 081 / 082 add agent_runtime.timezone (TEXT NOT NULL DEFAULT 'UTC') and rebuild the rollup pipeline (window function and both trigger functions) to compute bucket_date with AT TIME ZONE rt.timezone instead of bare DATE(). * No historical backfill — task_usage_daily rows already on disk keep their UTC bucket_date; only future writes / re-touches recompute under the new tz. (Product call from MUL-1950: 'guarantee future correctness'.) * runtime_usage.sql gains a @tz parameter on ListRuntimeUsage and GetRuntimeUsageByHour and threads tz through GetRuntimeTaskHourly Activity. ListRuntimeUsageDaily reads bucket_date as-is since the rollup already wrote it in tz. * parseSinceParamInTZ replaces the raw N×24h cutoff with start-of- day-N in the runtime's tz so 'last 7 days' lines up with bucket boundaries. * Daemon registration sends the host's IANA tz (TZ env, then time.Local), and UpsertAgentRuntime preserves any user override via a CASE-on-existing-value pattern so a daemon reconnect can't silently revert the operator's setting. * New PATCH /api/runtimes/:id endpoint (UpdateAgentRuntime) lets the runtime detail page edit the tz; the editor seeds with the browser tz on first interaction. Refs: MUL-1950 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix: harden runtime timezone rollups Co-authored-by: multica-agent <github@multica.ai> * fix: address runtime timezone review nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica.ai> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Eve <eve@multica-ai.local> |
||
|
|
39e57b870f |
fix(cli): allow --mode run_only on autopilot create/update (#2360)
* fix(cli): allow --mode run_only on autopilot create/update The autopilot run_only dispatch path is wired end-to-end (handler accepts the mode, AutopilotService.dispatchRunOnly enqueues a task with AutopilotRunID, daemon resolves workspace via autopilot_run -> autopilot in ClaimTaskByRuntime and TaskService.ResolveTaskWorkspaceID). The CLI guard was added before those fixes landed and never removed. Drop the CLI rejection on both create and update so callers can pick the same modes the API and UI already support, and remove the stale "unstable" callout from the autopilots docs. Closes multica-ai/multica#2347 Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): advertise autopilot run_only in agent runtime instructions The runtime config injected into AGENTS.md / CLAUDE.md only listed `--mode create_issue` for autopilot create and didn't expose `--mode` on update at all. So even after the CLI guard was lifted, agents reading their harness instructions would still believe create_issue was the only choice — undermining the "agents operate the same surface as humans" intent. Update both lines to advertise create_issue|run_only on create and on update, and add an InjectRuntimeConfig assertion so the runtime prompt can't drift away from the CLI surface again. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
15c3886302 |
docs(daemon): refresh stale comment for inline system prompt path (#2362)
The inline path now carries the full runtime brief (CLI catalog, workflow steps, persona, skills, project context) rather than just identity/persona instructions, after #2353 / #2355. The pre-existing comment still described it as "identity/persona instructions inline", which would mislead future maintainers about why the inline payload is load-bearing. Also call out kiro/kimi alongside openclaw/hermes since they were added to providerNeedsInlineSystemPrompt in #2328, and document the concrete failure mode (issues stuck in todo) so the rationale is searchable. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a6968c7485 |
fix(daemon): inline runtime brief for providers that need system prompt (#2355)
InjectRuntimeConfig writes the full meta skill content (CLI catalog, workflow instructions, project context, skills) to workdir/AGENTS.md, but providers like OpenClaw, Hermes, Kiro, and Kimi read bootstrap files from their own agent workspace — not the task workdir. The inline system prompt path (providerNeedsInlineSystemPrompt) only passed the agent persona instructions, so these providers never received the runtime brief. Have InjectRuntimeConfig return the rendered content so the daemon can both write it to disk (for file-reading providers) and pass it inline (for workspace-isolated providers). This avoids double-rendering and keeps the file and inline payloads identical. Fixes #2353 |
||
|
|
d713b57072 |
fix(daemon): add kiro and kimi to providerNeedsInlineSystemPrompt whitelist (#2328)
Kiro and Kimi share Hermes' ACP architecture and already accept SystemPrompt prepended in front of the user prompt (kiro.go:244-247, kimi.go:256-257). Without daemon-side opt-in, ExecOptions.SystemPrompt is never set, so per-task agent identity instructions are lost in deployments that rely on inline injection (e.g. K3 Lens-style daemon → wrapper → docker compose exec acp). Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c57546159d |
fix(daemon): mark provider 429 / out-of-credit agent runs as failed, not completed (#2323)
* fix(daemon): mark provider 429 / out-of-credit runs as failed, not completed
Two bugs combined to silently report failed agent runs as
"Completed" in the UI when the upstream LLM returned a 4xx (e.g.
HTTP 429 rate-limit / no credit on the account).
1. ACP backends (hermes, kimi, kiro) only promoted the run status to
"failed" when their stderr sniffer fired AND the agent output
buffer was empty. But hermes injects a synthetic agent text turn
("API call failed after 3 retries: HTTP 429...") on retry
exhaustion, so the buffer was never empty in the rate-limit
case and the promotion never ran. Drop the empty-output
precondition: the sniffer's regex (HTTP-status markers, named
error types) is specific enough to trust on its own.
2. The daemon's task-result switch only routed "blocked" through
FailTask; every other status — including "cancelled", and any
future status we forget to enumerate — fell through to
CompleteTask. Invert it so only an explicit "completed" status
reports success, and extract the switch into reportTaskResult
for direct testing. Cancelled now defaults to failure_reason
"cancelled" instead of being silently completed.
Closes GitHub multica#1952.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): only promote ACP run to failed on terminal provider error
Address GPT-Boy's review on the multica#1952 fix. The previous
promotion rule ("any sniffer line → fail") was too broad: the
existing sniffer also captures transient per-attempt warnings
("API call failed (attempt 1/3): RateLimitError [HTTP 429]"), and
those lines stay in the buffer for the rest of the run. A retry
sequence whose first attempt blipped but whose third attempt
succeeded would have been wrongly reported as failed.
Tighten the criteria with two additional signals, both defined on
the existing acpProviderErrorSniffer / output buffer:
- acpTerminalErrorRe — sticky `terminal` flag set when stderr shows
an exhausted/non-retryable marker (❌, [ERROR], "after N retries",
Non-retryable, BadRequestError, AuthenticationError). Per-attempt
warnings deliberately don't match.
- acpAgentOutputTerminalRe — matches the synthetic "API call failed
after N retries..." turn that hermes-style adapters inject into
the agent text stream when they give up; this catches multica#1952
even if hermes' stderr only logged transient attempts.
Promotion logic becomes a shared helper, promoteACPResultOnProviderError,
called from hermes / kimi / kiro. Promotes when (a) terminalMessage
is non-empty, (b) output contains the synthetic give-up turn, or
(c) output is empty and the sniffer captured anything at all
(preserves the original empty-output safety net for transient-only
sequences with no real result to fall back on).
Tests:
- TestHermesProviderErrorSnifferTerminalVsTransient — transient
attempt 1/3 alone returns terminalMessage="" but message!="";
a follow-on terminal marker flips terminal on.
- TestHermesProviderErrorSnifferTerminalNonRetryable — confirms
BadRequest / Authentication / Non-retryable / ❌ / [ERROR] are
classified terminal even on the very first attempt.
- TestHermesBackendDoesNotPromoteOnTransientRetry — fake hermes
emits attempt 1/3 to stderr then a normal agent text turn and
end_turn; resulting Status must stay "completed".
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
003dfd9b4b |
feat(quick-create): add project picker that remembers last pick (#2321)
* feat(quick-create): add project picker that remembers last pick Quick-create users targeting one project repeatedly had to restate "in project X" in every prompt. The modal now exposes a project picker beside the agent picker, persists the selection per-workspace, and pins the agent's `multica issue create` invocation to that project so the prompt text doesn't have to. The picked project also flows to the daemon as ProjectID/ProjectTitle and its github_repo resources override the workspace repo fallback — same treatment issue-bound tasks already get. Co-authored-by: multica-agent <github@multica.ai> * fix(quick-create): move project picker into property pill row Reviewer feedback: the picker felt out of place wedged next to the agent header. Move it into a property toolbar row above the footer, reusing the shared `ProjectPicker` + `PillButton` so its placement and styling line up exactly with the manual create panel. This also drops the bespoke dropdown / aria / label strings that were only needed while the picker rendered inline beside "Created by". Co-authored-by: multica-agent <github@multica.ai> * fix(quick-create): clear stale persisted project + carry across mode switch Two review-blocking bugs in PR #2321: 1. The stale-id sweep in AgentCreatePanel only fired when projects.length > 0 and only cleared local state, leaving lastProjectId pointing at a deleted project. The next open re-seeded the dead UUID and submit hit the server's `project not found` rejection. Gate on the query's `isSuccess` so we can tell "loading" apart from "loaded as empty", and clear both local state and the persisted preference when the selection isn't in the resolved list. 2. ManualCreatePanel's switchToAgent dropped the picked project from the carry payload, so flipping manual → agent silently fell back to the agent panel's own lastProjectId — potentially routing the issue to a different project than the one shown in manual mode. Forward project_id alongside prompt / agent_id, and add a regression test. Co-authored-by: multica-agent <github@multica.ai> * test(quick-create): pass new isExpanded props in stale-project tests Main got an expand button on AgentCreatePanel via #2320 while this branch was open, adding `isExpanded` / `setIsExpanded` to the panel's required props. The two new stale-project tests still passed `{ onClose }` only, which CI's typecheck (run on the main+branch merge) caught while my local run did not. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3f20999597 |
refactor(timeline): drop server-side comment + timeline pagination (#2322)
* refactor(timeline): drop server-side comment + timeline pagination (MUL-1929) The cursor-paginated /timeline and /comments endpoints were sized for a problem the data shape doesn't have: prod p99 is ~30 comments per issue and the all-time max is ~1.1k. Time-based pagination also splits reply threads across page boundaries (orphan replies), which the frontend was papering over with an "orphan rescue" that promoted disconnected replies to top-level — confusing UX with no real benefit. Replace both endpoints with a single full-issue fetch, capped server-side at 2000 rows as a defensive safety net (never hit in practice). Server - /api/issues/:id/timeline now returns a flat ASC TimelineEntry[] (matches the legacy desktop contract — older Multica.app builds keep working because the wrapped TimelineResponse + cursors are gone, and the raw array shape was always what they consumed). - /api/issues/:id/comments drops limit/offset; only ?since is honoured for the CLI agent-polling flow. - Drop ListCommentsBefore/After/Latest, ListActivitiesBefore/After/Latest and the timelineCursor encoding. - Replace with ListCommentsForIssue / ListCommentsSinceForIssue / ListActivitiesForIssue (capped by argument). CLI - multica issue comment list drops --limit / --offset and the X-Total-Count reporting; --since is preserved for incremental polling. Frontend - Replace useInfiniteQuery with useQuery in useIssueTimeline; drop fetchOlder/Newer, jumpToLatest, isAtLatest, newEntriesBelowCount. - Remove timeline-cache helpers (mapAllEntries / filterAllEntries / prependToLatestPage) and the TimelinePage / TimelinePageParam types. - WS event handlers update the single flat-array cache directly. - Drop the orphan-reply rescue in issue-detail — every reply's parent is now guaranteed to be in the same array. - Strip the "show older / show newer / jump to latest" buttons and their i18n strings. Co-authored-by: multica-agent <github@multica.ai> * fix(timeline): address review feedback on pagination removal Three issues caught in PR #2322 review: 1. /timeline broke for stale clients between #2128 and this PR. They send ?limit/?before/?after/?around and parse with the wrapped TimelinePageSchema; the new flat-array response was failing schema validation and falling back to an empty timeline. Restore the wrapped shape on those query params (DESC entries, null cursors, has_more_*=false), keeping the flat ASC array for bare requests. Around-mode now also fills target_index from the merged slice so legacy clients can still scroll-to-anchor without a follow-up. 2. The agent prompts in runtime_config.go and prompt.go still told agents that `multica issue comment list` accepts --limit/--offset and to use `--limit 30` on truncated output. With those flags removed in this PR, new agent runs would hit "unknown flag" or skip context. Update the prompt copy to "returns all comments, capped at 2000; --since for incremental polling". 3. useCreateComment's onSuccess was a bare append to the timeline cache with no id-dedupe, so a fast comment:created WS event firing before onSuccess produced a transient duplicate. Restore the id guard the old prependToLatestPage helper used to provide. Adds two new boundary tests: - TestListTimeline_LegacyWrappedShape_OnPaginationParams - TestListTimeline_LegacyWrappedShape_AroundFillsTargetIndex Co-authored-by: multica-agent <github@multica.ai> * test(handler): fix timeline test assertions for handler-package isolation The TestListTimeline_* assertions assumed CreateIssue would seed an "issue_created" activity_log row, but the activity listener that publishes those rows is registered in cmd/server/main.go — handler-package tests don't wire it up. CI saw 5 entries (3 comments + 2 activities) where the test expected ≥6. Drop the auto-activity assumption: assert exactly 5 entries in TestListTimeline_MergesCommentsAndActivities, and tighten TestListTimeline_EmptyIssue to assert a fully-empty timeline. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6d9ebb0fdd |
fix(daemon): unblock issues stuck on a poisoned-image agent session (#2314)
* fix(daemon): treat upstream API 400 invalid_request_error as poisoned session A markdown-linked image in an issue description that the agent downloads as a tiny CDN auth-error file and Read's as a PNG poisons the conversation: the LLM API rejects the bad image with 400 invalid_request_error, the session_id is pinned mid-flight, and every follow-up task on the issue (comment-trigger, auto-retry) resumes the same poisoned conversation and hits the same 400 — the issue can no longer be executed even after the description is cleaned up. Mirror the existing fallback-output classifier on the error side: detect "API Error: ... 400 ... invalid_request_error" in the agent error string, persist failure_reason='api_invalid_request', and add it to the GetLastTaskSession exclusion list so the next task starts a fresh session that re-reads the (now-clean) description. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): unblock issues already poisoned by API 400 invalid_request_error The forward-only classifier from the previous commit only tags new failures. Issues like MUL-1918 already have multiple failed-task rows whose failure_reason is the pre-fix default 'agent_error', and GetLastTaskSession falls back to those legacy rows on the next claim — so deploying the classifier alone leaves existing poisoned issues stuck (GPT-Boy review on PR #2314). Two complementary changes: - Migration 079 backfills failure_reason='api_invalid_request' on every pre-existing 'agent_error' row whose error text matches the canonical Anthropic 400 invalid_request_error shape. Keeps observability consistent (multica issue runs / UI now report the right reason). - GetLastTaskSession adds a defensive ILIKE clause on error text. Closes the deploy-window gap where the old binary could write a new 'agent_error' row between the migration running and the new code taking over, and protects against future error-format variants the daemon classifier might miss. Plus regression tests covering the legacy + new coexistence case GPT-Boy flagged, and a guard rail asserting benign 'agent_error' failures (timeouts, tool errors) still resume their session. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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> |
||
|
|
61ce8a8090 |
feat(daemon): add disk-usage CLI to surface per-task / per-workspace footprint (#2267)
* feat(daemon): add disk-usage CLI to surface per-task / per-workspace footprint Adds `multica daemon disk-usage [--by-workspace] [--by-task] [--top N] [--output json]`, walking the workspaces root to report task and workspace disk consumption without requiring a running daemon. Sizing reuses the GC artifact patternSet (basename-only) so the reported "artifact" footprint matches what `cleanTaskArtifacts` would actually reclaim, and the walk honors the same safety contract: never enters .git, never follows symlinks, counts only regular files. Refactors WorkspacesRoot resolution into an exported `ResolveWorkspacesRoot` so the read-only CLI picks the same root the running daemon would have. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): distinguish displayed totals from scan totals; add workspace artifact ratio - Track scan-wide TotalTaskCount / TotalWorkspaceCount on the report so `--top N` no longer leaves the table footer claiming the truncated row count is the full count. The CLI now prints a "Showing top N of M … Displayed: X. Scan total: Y" line whenever truncation happens, and keeps the bare "Total: …" footer for the un-truncated case. - Add ArtifactRatio (0..1) on WorkspaceDiskUsage and TotalArtifactRatio on the report. The workspace table renders an `ARTIFACT %` column. ratio() guards size=0 so empty workspaces report 0% instead of NaN%. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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>
|
||
|
|
9a3a99cef8 |
fix: make CLI short IDs routable
Make CLI table IDs routable across issue, autopilot, project, label, and task-run workflows. Adds scoped UUID-prefix resolution, --full-id table options, issue KEY display, safer actor/name output, and updated CLI docs/runtime prompt. |
||
|
|
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> |
||
|
|
d964d37f97 |
Revert "fix(cli): add --content-file / --description-file for non-ASCII on Wi…" (#2252)
This reverts commit
|
||
|
|
9650788709 |
fix(cli): add --content-file / --description-file for non-ASCII on Windows (#2247)
* fix(cli): add --content-file / --description-file for non-ASCII on Windows Windows PowerShell 5.1 (the Win11 default) and cmd.exe re-encode HEREDOC content through the active console codepage before piping it to a child process. Characters the codepage cannot represent are silently replaced with `?`, so agents on Chinese Win11 hosts emitting `--content-stdin` / `--description-stdin` HEREDOCs land all of their Chinese as `?` in the issue body and comments. The daemon log shows the original Chinese correctly because slog writes to a file directly, so the regression hides until the user opens the issue page. Add a `--content-file <path>` / `--description-file <path>` source to `resolveTextFlag`: the CLI reads the file straight off disk, preserves UTF-8 bytes verbatim, and skips the shell entirely. The runtime config injected into AGENTS.md / CLAUDE.md now surfaces this as the canonical Windows fallback when the daemon host runs on Windows; non-Windows hosts keep the existing stdin/HEREDOC guidance untouched. Closes #2198, #2236. Co-authored-by: multica-agent <github@multica.ai> * fix(execenv): route every Windows-host stdin directive at --content-file GPT-Boy on PR #2247 caught that the previous patch only inserted a Windows fallback into the Available Commands section. Two later prompt surfaces still hard-coded `--content-stdin` and overrode it for the agent: - The Codex-specific paragraph in `buildMetaSkillContent`, which always said "always use `--content-stdin` with a HEREDOC". - `BuildCommentReplyInstructions`, which is re-emitted on every turn for comment-triggered tasks (both via the AGENTS.md/CLAUDE.md workflow and the daemon's per-turn prompt) and mandated the same HEREDOC pipe. On Windows hosts we now branch both surfaces to a file-based template: the agent writes the body to a UTF-8 file with its file-write tool and posts via `--content-file <path>`. Non-Windows hosts keep the existing stdin/HEREDOC guidance untouched. Tests: - `TestBuildCommentReplyInstructionsWindowsUsesContentFile` pins the Windows / non-Windows reply-instruction text directly. - `TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin` asserts that the end-to-end CLAUDE.md / AGENTS.md surface for a comment-triggered Windows task has no remaining `--content-stdin` directive that could override the Windows fallback (covers Claude + Codex providers). Co-authored-by: multica-agent <github@multica.ai> * fix(execenv): make Windows comment block file-first, pin tests by GOOS GPT-Boy's second review on PR #2247 flagged two follow-up blockers: 1. The Windows comment/description block in `buildMetaSkillContent` was "stdin first, file caveat appended" — agents on Windows still saw "Agent-authored comments should always pipe content via stdin" / "MUST pipe via stdin" / `--description-stdin` directives before reaching the Windows fallback, so the contradicting instruction was live in the same prompt. Rewrite the entire Available Commands bullet for Windows hosts as file-first: the headline line names `--content-file`, the bulleted rules name `--content-file` / `--description-file`, and stdin only appears in anti-prescriptive "do NOT pipe via …" prose. 2. The existing non-Windows tests (TestBuildCommentReplyInstructions IncludesTriggerID, TestInjectRuntimeConfigDirectsMultiLineWritesToStdin, TestInjectRuntimeConfigCodexEmphasizesStdinForFormattedComments, TestInjectRuntimeConfigCommentTriggerUsesHelper) all depended on `runtimeGOOS` defaulting to non-Windows; they would silently fail on a Windows test runner. Pin them to `runtimeGOOS = "linux"` via save+restore and drop t.Parallel so they don't race with the GOOS-mutating Windows tests. Test additions: - TestInjectRuntimeConfigWindowsRecommendsContentFile now asserts the Windows AGENTS.md does NOT contain prescriptive stdin phrasings (`MUST pipe via stdin`, `use --description-stdin and pipe a HEREDOC`, `<<'COMMENT'`, `Agent-authored comments should always pipe content via stdin`, `always use --content-stdin`) on top of the file-first positive assertions. The ban list pins prescriptive substrings, not bare flag names, so anti-prescriptive prose like "do NOT pipe via --content-stdin" doesn't trip the ban. - TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin gets the same expanded ban list across the Available Commands, Codex paragraph, and per-turn reply template surfaces. - The non-Windows side of TestInjectRuntimeConfigWindowsRecommendsContentFile pins that the Linux stdin/HEREDOC contract is still in place, so a future refactor can't accidentally move every host to file-first. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2f979ac6f0 |
fix(daemon): tighten quick-create prompt to drop meta-instructions and apologetic Context (#2137)
* fix(daemon): tighten quick-create prompt to drop meta-instructions and apologetic Context
The quick-create prompt was producing descriptions that:
1. Echoed routing meta-instructions ("create an issue for me", "cc @X") into
the User request body, even though those phrases are handled by separate
CLI flags and are not spec content.
2. Emitted a Context section to apologize for resources it could not fetch
(e.g. an image attachment not piped through to the run), instead of
staying silent and letting the executing agent ask the user.
3. Preserved pure conversational fillers ("对吧?", "嗯", "那个…") because the
model treated removing them as forbidden paraphrasing.
Updates the prompt to call out each of these as explicit non-spec material
to strip before writing the description, while keeping the "high fidelity /
no paraphrasing of substantive content" invariant. Adds a regression test
that locks in the new rules at the substring level.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): preserve cc mention links in quick-create description
Stripping "cc @Y" wholesale would have lost the mentioned member's only
routing channel: `multica issue create` has no --subscriber/--cc flag, and
the platform auto-subscribes members by parsing `[@Name](mention://member/<uuid>)`
links from the description body. Without the mention link in the body, a
cc'd member would never get subscribed or notified.
Updates the prompt to:
- Strip only the verbal "cc" wrapper from the User request body.
- Append a trailing `CC: <mention links>` line to the description so the
platform's auto-subscribe logic still picks the mentions up.
- Spell out the contrast for assignee mentions, where --assignee-id is
the routing channel and the body should not double-encode the mention.
Also adds a substring assertion for the "Pure conversational fillers" rule
that was missing from the original regression test.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(daemon): trim quick-create prompt rules to general principles
Reviewers pointed out the previous rewrite traded one prompt smell (over-
permissive verbatim quoting) for another (too many specific rules and
exhaustive bilingual example tables). Rewrites the description block as
general principles with a single representative example each, trusting the
model to generalize:
- "Strip non-spec material before writing" replaces the multi-bullet list
of routing-meta-instruction and conversational-filler enumerations.
- "Include Context only when references were fetched and produced facts;
never use it as an apology log" replaces the three "Do NOT emit a
Context section to" sub-bullets.
- The CC exception (the only operationally non-obvious rule, since
`multica issue create` has no --subscriber flag) is kept inline as a
single sentence and is still locked in by the regression test.
Net: ~16 fewer lines of prompt text without losing any of the rules the
test asserts.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
ba147708a6 |
fix(timeline): cursor-paginated timeline to stop long-issue freeze (#1968) (#2128)
* fix(timeline): cursor-paginated timeline to stop long-issue freeze (#1968) Opening an issue from Inbox with thousands of timeline entries used to hard-freeze the browser tab on a synchronous render of every comment + activity. The whole pipeline was unbounded: the API returned every row, TanStack Query cached the full array, and IssueDetail mounted N CommentCards (each running a full react-markdown + lowlight pipeline) in one frame. This swaps the timeline endpoint to keyset cursor pagination and rewires the frontend to useInfiniteQuery so a long issue costs the same as a short one on first paint. API: - GET /issues/:id/timeline now accepts ?before / ?after / ?around (mutex) + ?limit (default 50, max 100); response wraps entries with next/prev cursors and has_more flags. Cursors are opaque base64 (created_at, id). - ?around=<entry_id> anchors a window on the target so Inbox notifications pointing at an old comment never trigger the freeze. - New composite indexes on (issue_id, created_at DESC, id DESC) replace the redundant single-column ones so keyset queries are index-only scans. - /issues/:id/comments default branch now caps at 50 instead of returning every row unbounded; the unbounded ListComments / ListActivities sqlc queries are deleted. Frontend: - useIssueTimeline switches to useInfiniteQuery, exposes fetchOlder/fetchNewer/jumpToLatest + isAtLatest + newEntriesBelowCount. - WS handlers respect the at-latest invariant: comment/activity:created prepends to pages[0] only when the user is reading the live tail; otherwise it just bumps a counter so the UI offers a "Jump to latest" affordance without yanking scroll. - Optimistic mutations adapted to the InfiniteData shape via shared helpers (mapAllEntries / filterAllEntries / prependToLatestPage in core/issues/timeline-cache.ts) and use setQueriesData so all open windows of the same issue stay in sync. - IssueDetail Activity section gets a TimelineSkeleton placeholder during the brief load window plus subtle text-link load-more buttons matching the existing Subscribe affordance (no Button chrome). Top uses a divider for boundary clarity; bottom shows "Jump to latest · N new" weighted slightly heavier when there's unread state. - highlightCommentId now flows into the hook's around parameter so Inbox jumps fetch the surrounding 50 entries directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(agent): default comment list to 50 + prompt hint about long issues The CLI's "multica issue comment list" used to default to --limit 0 (meaning "fetch every comment"), which lets an agent on a long issue fill its context window with thousands of rows. The default is now 50; agents that need older history can pass --limit or --since explicitly. The local-coding-agent prompt also gains a single-line note about this in both the comment-triggered and on-assign flows so the agent knows to scope its fetches when issue size is unknown. Autopilot run-only mode is intentionally unchanged — it has no issue context to query. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |