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>
The CLI auth callback was hardcoded to localhost, breaking self-hosted
setups where the browser runs on a different machine than the CLI.
- CLI: derive callback host from configured app URL; bind to 0.0.0.0
when the app URL is not localhost so remote browsers can reach it
- Frontend: expand validateCliCallback to accept RFC 1918 private IPs
(10.x, 172.16-31.x, 192.168.x) in addition to localhost
Closes#923
* 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.
* fix(daemon): use runtime's owner_id for agent migration, not caller's
The migration was gated on ownerID.Valid which is only true for PAT/JWT
registrations. Daemon token registrations (the common case for background
daemon restarts) had ownerID as zero, skipping migration entirely.
Fix: use registered.OwnerID (preserved via COALESCE on upsert) instead
of the caller's ownerID. This ensures migration runs even when the daemon
re-registers via daemon token after an upgrade.
On Windows, `cmd /c start <url>` treats `&` in the URL as a shell
command separator, truncating the login URL at the first `&cli_state=`
parameter. This causes the OAuth state validation to fail silently,
requiring users to login a second time.
Adding an empty title argument (`""`) before the URL is the standard
Windows fix — `start` interprets the first quoted argument as a window
title, so without it, URLs containing special characters get mangled.
When a user cancels an issue, active agent tasks now get cancelled
automatically. Previously, task cancellation only triggered on assignee
changes — the cancelled status was incorrectly treated like any other
agent-managed status transition.
Closes#926
* fix(storage): scope S3 upload keys by workspace
Upload keys now use `workspaces/{workspace_id}/{uuid}.{ext}` instead of
flat `{uuid}.{ext}`, isolating file storage per workspace. Files uploaded
without workspace context (e.g. avatars) keep the flat key structure.
Refs: MUL-577
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(storage): scope user uploads under users/{user_id}/ prefix
Non-workspace uploads (avatars, profile images) now use
`users/{user_id}/{uuid}.{ext}` instead of flat `{uuid}.{ext}`,
matching the workspace-scoped pattern from the previous commit.
Refs: MUL-577
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(storage): fix LocalStorage for nested key paths
- Add MkdirAll before WriteFile to create intermediate directories
for workspace/user-scoped keys
- Fix KeyFromURL to preserve full path after /uploads/ prefix instead
of stripping to just the filename
- Update tests to match new behavior
Refs: MUL-577
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(upload): validate ownership before writing to storage
Move Storage.Upload after issue_id/comment_id ownership validation
to prevent orphaned files in S3 when validation fails. Previously,
the file was uploaded first and validation happened after, leaving
files in workspace-scoped S3 prefixes even on rejected requests.
Refs: MUL-577
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(upload): restore workspace membership check before upload
The membership check was accidentally removed during the upload
reordering refactor. Without it, any authenticated user could upload
files to any workspace by setting the X-Workspace-ID header.
Also restores the comment explaining the 200-on-DB-error behavior.
Refs: MUL-577
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>
Replace Tiptap's BubbleMenu plugin with @floating-ui/react-dom for
all floating editor UI (formatting toolbar, link preview cards).
Architecture:
- useFloating({ strategy:"fixed" }) + createPortal(body) escapes
all overflow:hidden ancestors (Card component, scroll containers)
- autoUpdate + contextElement monitors all scroll ancestors for
repositioning; manual update() on transaction for virtual ref changes
- open prop resets isPositioned on visibility change (no stale-position
flash at 0,0)
- display:none for hiding (not return null which causes blur/focus
cycle, not visibility:hidden which leaves transition artifacts)
- No blur listener — portal DOM updates cause false editor blurs;
outside-click + scroll + resize + Escape handle all close cases
Bug fixes:
- BubbleMenu: remove all custom visibility hacks, let selection state
drive show/hide
- Link preview: new shared card (Copy + Open) for editable editor and
readonly markdown, portaled to body with fixed positioning
- TitleEditor: use JSON content format (not HTML interpolation that
loses < > characters)
- Blob URLs: strip from getMarkdown output during upload
- Markdown paste: check clipboard.files first to avoid intercepting
file paste events
- FileCard: escape HTML attributes in preprocessing
- Link extension: enable linkOnPaste, set defaultProtocol to https,
switch URL normalization to protocol blocklist (only block
javascript:/data:/vbscript:)
Dependencies: add @floating-ui/react-dom, remove @tiptap/extension-bubble-menu
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(server): validate workspace membership for subscription targets and file uploads
Closes MED-1 (cross-workspace subscription injection) and MED-2 (file upload
missing workspace member validation) from the security audit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(server): add negative tests for cross-workspace subscription and upload
Address PR review feedback:
- Add tests verifying cross-workspace user_id is rejected with 403 on
subscribe and unsubscribe
- Add test verifying upload with foreign workspace_id is rejected with 403
- Make isWorkspaceEntity explicitly enumerate "member"/"agent" and reject
unknown user types
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>
- Make --content and --content-stdin mutually exclusive with explicit error
- Use TrimSuffix instead of TrimRight to only strip the trailing newline
- Return "stdin content is empty" instead of misleading "required" error
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Check runCtx.Err() before readErr/waitErr so that context-driven
process kills (timeout, user cancellation) report the correct status
("timeout" or "aborted") instead of "failed".
When exec.CommandContext kills the gemini process, io.ReadAll can
return a non-nil error as a side-effect of the closed pipe. The
previous code checked readErr first, masking the real cause. This
aligns gemini.go with the ordering already used in claude.go and
hermes.go.
Fixes#914
- Guard handleDownload to only trigger from "available" state
- Only allow dismiss when update is available, not during download/ready
- Use shadcn design tokens (text-success) instead of hardcoded colors
* fix(cli): auto-create workspace for new users during setup
When a new user runs `multica setup` and has no workspaces,
the onboarding flow now auto-creates a default workspace
(named "<name>'s Workspace") instead of failing when the
daemon tries to start with zero watched workspaces.
As a safety net, setup commands also skip daemon start
gracefully if no workspaces are configured, instead of
erroring out.
* fix(cli): redirect to web onboarding instead of auto-creating workspace
When no workspaces exist, the CLI now opens the web onboarding wizard
in the browser and polls until the user completes workspace creation.
This reuses the existing 4-step onboarding flow (workspace → runtime →
agent → done) instead of duplicating creation logic in the CLI.
* fix(cli): address code review — token login crash and misleading success msg
1. Token login (`multica login --token`) on a fresh account no longer
crashes: waitForOnboarding uses tryResolveAppURL (returns "" instead
of os.Exit(1)) and falls back to printing manual instructions.
2. Setup commands no longer print "✓ Setup complete!" when onboarding
was not finished. Shows "⚠ Setup incomplete" with next steps instead.
* 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.
Combined P0 and P1 improvements to the OpenClaw agent backend, informed
by PaperClip's adapter architecture:
P0 — User experience:
- Streaming output — emit MessageText as NDJSON events arrive in real
time, instead of waiting for the final result blob
- Tool use support — parse and emit MessageToolUse/MessageToolResult
from streaming events, matching Claude and OpenCode backends
- Model & system prompt — pass --model and --system-prompt to the
OpenClaw CLI when configured
P1 — Robustness:
- Hardened JSON parsing — tryParseOpenclawResult requires lines to
start with '{', eliminating fragile brace-scanning that could
false-match JSON fragments in log lines
- Lifecycle event handling — new "lifecycle" event type with phase
tracking (error/failed/cancelled), plus structured error objects
(error.name, error.data.message) matching PaperClip's pattern
- Usage field name variants — parseOpenclawUsage supports multiple
naming conventions (input/inputTokens/input_tokens, cacheRead/
cachedInputTokens/cache_read_input_tokens, etc.) with incremental
accumulation across step_finish events
Backwards compatible with the legacy single JSON blob format.
31 tests covering all new functionality.
Closes MUL-726
* fix(auth): detect cookie-based session during CLI setup flow
When users run `multica setup` after logging into multica.ai, the CLI
redirects to the login page which only checked localStorage for an
existing session. Since the web app stores auth tokens as HttpOnly
cookies (not localStorage), the session was never detected and users
had to log in again.
Now the login page also tries `api.getMe()` (which sends the HttpOnly
cookie automatically) when no localStorage token exists. A new
`POST /api/cli-token` endpoint lets cookie-authenticated sessions
obtain a bearer token to hand off to the CLI.
* fix(auth): prioritise cookie auth over localStorage in CLI setup flow
Address code review feedback: cookie-first detection avoids authorising
the CLI with a stale or mismatched localStorage token. The useEffect now
calls getMe() without a bearer token first (relying on the HttpOnly
cookie), and only falls back to localStorage if cookie auth fails.
handleCliAuthorize uses an authSourceRef to pick the matching token
source — issueCliToken for cookie sessions, localStorage for token
sessions — preventing the click handler from re-reading a potentially
stale localStorage entry.
When NEXT_PUBLIC_WS_URL is not set, the WebSocket URL defaulted to
ws://localhost:8080/ws. This broke real-time features (chat streaming,
live updates, notifications) for self-hosted deployments accessed over
LAN — the browser tried connecting to localhost on the client machine
instead of the Docker host.
Now the web app derives the WebSocket URL from window.location, routing
through the existing Next.js /ws rewrite. This works for localhost, LAN,
and custom domain setups without any extra configuration.
Also adds NEXT_PUBLIC_WS_URL as a Docker build arg for explicit override,
and documents LAN access configuration in SELF_HOSTING_ADVANCED.md.
Closes#896
BREW_PACKAGE="multica-ai/tap/multica" was defined but never used.
All brew install/upgrade/list commands used the bare name "multica",
which could fail to resolve the correct tap formula. Replace all
occurrences with "$BREW_PACKAGE" to match the Go CLI (update.go)
and Makefile behavior.
Workspace creation with duplicate slugs now auto-appends -2, -3, … on
the server side instead of returning 409. The onboarding wizard also
shows an editable Workspace URL field (multica.ai/<slug>) that
auto-generates from the name but can be manually customized.
The Install-CliScoop function referenced multica-ai/scoop-bucket.git
which does not exist, causing errors for Windows users with Scoop
installed. Always use direct binary download instead.
Closes#880
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
Decouple install.sh from environment configuration — install.sh now only
installs the CLI binary (and optionally Docker via --with-server), while
all environment configuration moves to `multica setup` subcommands.
Key changes:
- install.sh: remove config writes, rename --local to --with-server
- multica setup: add cloud/self-host subcommands with --server-url,
--app-url, --port, --frontend-port flags and --profile support
- Add config overwrite protection with interactive prompt
- Remove redundant commands: `config local`, `auth login` alias
- Replace silent multica.ai fallbacks with explicit errors
- Onboarding wizard: dynamically show correct setup command for
Cloud vs Self-host environments
- Update all docs, landing page, and install scripts for consistency
Claude's stream-json flow can emit the terminal result event while the
child process still waits on open stdin. Closing stdin as soon as the
final result arrives lets the CLI exit cleanly instead of idling until
the daemon timeout fires.
Constraint: Must preserve the existing Claude stream-json protocol and child-process lifecycle
Rejected: Increase ping timeout only | masks the hang without fixing process exit
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep Claude stdin handling aligned with the stream-json terminal result semantics; do not defer closure until goroutine teardown
Tested: Reproduced self-hosted runtime ping timeout locally; verified ping succeeds after closing stdin on result; cd server && go test ./pkg/agent
Not-tested: Full make check; Bedrock/Vertex-specific Claude auth flows
Remove the archive button, active/archived grouping, and related
imports from ChatSessionHistory. This also fixes the nested <button>
hydration error (GitHub #875) since the inner archive Button was the
only nested button inside the row's outer <button>.
* fix(comment): set trigger_comment_id to actual reply, not thread root
When a user replies in a thread and @mentions an agent, the enqueued
task's trigger_comment_id was incorrectly set to the parent (thread
root) comment instead of the reply that contained the mention. This
caused the agent to read the wrong comment and miss the user's actual
instructions.
Always pass comment.ID to EnqueueTaskForMention so agents see the
comment that triggered them.
Fixes MUL-708
* fix(task): resolve thread root in createAgentComment for reply triggers
With trigger_comment_id now correctly pointing to the actual reply
(not the thread root), createAgentComment must resolve to the thread
root before posting. Otherwise error/system comments would have
parent_id pointing to a nested reply, making them invisible in the
frontend's flat thread grouping.
Part of MUL-708
Some OpenClaw JSON outputs contain durationMs but lack payloads field.
The original condition rejected these results, causing the agent to
return "openclaw returned no parseable output" instead of the actual
execution result.
Fix by accepting results that have either payloads OR durationMs > 0.
Fixes#830
Co-authored-by: leaderlemon <leaderlemon@users.noreply.github.com>
AuthInitializer only checked for multica_token in localStorage. In
cookie auth mode (introduced by the HttpOnly cookie migration), there
is no localStorage token — so AuthInitializer immediately set the user
to null and triggered a logout redirect on every page load/reload.
Add a cookieAuth code path that calls api.getMe() using the HttpOnly
cookie sent automatically by the browser, matching the auth store's
initialize() logic.
Fixes MUL-705, fixes#864