State management
- Pending task / live timeline are now Query-cache single source;
Zustand mirror removed (fixes duplicate assistant render caused by
the invalidate→refetch race window)
- WS subscriptions moved from ChatWindow to global useRealtimeSync so
pending state survives minimize and refresh
- New GET /chat/sessions/:id/pending-task to recover live state on mount
- Drafts persisted per-session (was per-workspace)
Unread tracking
- Migration 040: chat_session.unread_since (event-driven; old chats
stay clean — no mass backfill)
- POST /chat/sessions/:id/read clears unread; broadcasts
chat:session_read so other devices sync
- New GET /chat/pending-tasks aggregate for the FAB
- ChatFab: brand-color impulse animation while running, brand-dot
badge of unread session count
- ChatWindow auto-marks read when user is viewing the session
Header redesign
- Two independent dropdowns: agent (avatar + name + My/Others
grouping) at the input bottom-left; session (title + agent avatar)
in the header
- ⊕ new-chat button replaces the old + and history buttons
- Session dropdown lists all sessions across agents with avatars
- Empty state: 3 clickable starter prompts that send immediately
- Mention link renderer falls through to default span on null —
fixes @member/@agent/@all silently disappearing app-wide
- User messages render through Markdown
- Enter submits in chat input only (with IME guard + codeBlock skip);
bubble menu hidden in chat
Misc
- Partial index on agent_task_queue for fast pending-task lookup
- 2 new storage keys added to clearWorkspaceStorage
- useMarkChatSessionRead has onError rollback
- chat.* namespace logs across store, mutations, components, realtime
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a member replies in a member-started thread without @mentioning the
assigned agent, the on_comment trigger was suppressed — even if the agent
had already replied in that thread. This meant the common flow of
"member posts → agent replies → member follows up" would not re-trigger
the agent on the follow-up.
Add HasAgentRepliedInThread SQL query and check it in isReplyToMemberThread
so that agent participation in a thread is treated as an ongoing conversation.
When a comment-triggered task resumes an existing session, the agent
may mistake the new comment for a previous one and skip it. Add [NEW
COMMENT] tag to the prompt and reinforce in AGENTS.md workflow that
the agent must respond to THIS specific comment, not prior ones.
When a task finishes between the UI rendering the Stop button and the
user clicking it, CancelAgentTask returns no rows. Previously this
surfaced as a 400 error. Now CancelTask checks for pgx.ErrNoRows and
returns the current task state instead of an error.
Closes#954
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>
* 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.
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>
* 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>
* 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(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.
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.
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
* 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
- cmd_daemon.go: use filepath.Join for PID/log file paths instead of string concat with "/"
- codex_home.go: use os.TempDir() instead of hardcoded "/tmp" for cross-platform fallback
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sub-issue progress indicator (e.g. "0/2") was undercounting because
it was computed from the client-side issue list, which only loads the
first 50 done issues. Sub-issues marked as done beyond that page were
excluded from both the total and done counts.
Added a dedicated backend endpoint (GET /api/issues/child-progress) that
aggregates child issue counts directly from the database, ensuring
accurate totals regardless of client-side pagination or filtering.
Fixes MUL-702
On Windows, os.Symlink requires Developer Mode or admin privileges.
Extract symlink creation into platform-specific files: on non-Windows,
behavior is unchanged (os.Symlink). On Windows, try os.Symlink first,
then fall back to directory junctions (mklink /J) for dirs and file
copy for files.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The frontend's listTaskMessages() was calling /api/daemon/tasks/{id}/messages
which uses DaemonAuth middleware (requires Authorization header). After the
cookie auth migration (#819), cookie-mode sessions don't send an Authorization
header, causing 401 on this endpoint. The 401 then triggers handleUnauthorized()
which clears the workspace context, cascading into 400 errors on all subsequent
requests.
Fix: add GET /api/tasks/{taskId}/messages under regular user auth middleware,
and update the frontend to use it instead of the daemon endpoint.
Closes#833
* feat(onboarding): add full-screen onboarding wizard for new workspaces
Replace auto-provisioned workspace with an interactive 4-step onboarding
wizard: Create Workspace → Connect Runtime → Create Agent → Get Started.
- Remove server-side ensureUserWorkspace() so new users land in onboarding
- Add onboarding wizard in packages/views/onboarding/ (4 steps)
- Wire login/OAuth callbacks to redirect to /onboarding when no workspace
- Add DashboardGuard onboardingPath fallback for workspace-less users
- Sidebar "Create workspace" navigates to /onboarding instead of modal
- Remove CreateWorkspaceModal (replaced by wizard step 1)
- Auto-generate workspace slug from name (no user-facing URL field)
- Unified CLI install flow: install.sh + multica setup (auto-detects local)
- Create onboarding issues on completion with interactive "Say hello" task
* test(auth): update workspace tests to match onboarding flow
Login no longer auto-creates workspaces — new users start with zero
workspaces and create one through the onboarding wizard. Update both
integration and handler tests to assert 0 workspaces after verify-code.
1. Security: add isBlockedEnvKey() blocklist that rejects MULTICA_*
prefix and critical system vars (HOME, PATH, USER, SHELL, TERM,
CODEX_HOME) from custom_env injection
2. Observability: log warnings when json.Unmarshal fails on custom_env
(agentToResponse + claim endpoint)
3. UX: use stable auto-increment IDs for env entry React keys instead
of array index to prevent input focus/state issues on add/remove
* fix(security): use first-message auth for WebSocket instead of URL query param
Token was exposed in URL query parameters (HIGH-4 from security audit),
visible in server/proxy logs, browser history, and referrer headers.
Now non-cookie clients (desktop, CLI) send the token as the first
WebSocket message after the connection opens. Cookie-based auth (web)
continues to work unchanged. Server-side auth priority flipped to
cookie-first.
Closes MUL-580
* fix(security): add auth_ack and fix test JSON construction
Server sends auth_ack after successful first-message auth so the client
knows auth completed before firing reconnect callbacks. Test now uses
json.Marshal instead of string concatenation for the auth message.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(test): update WebSocket integration test for first-message auth
The integration test still passed the token as a URL query param,
causing a timeout since the server now expects first-message auth
for non-cookie clients.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: yushen <ldnvnbl@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(issue): default create status to todo instead of backlog
Issues created without an explicit status now default to `todo` so the
local daemon picks them up immediately. Previously they defaulted to
`backlog`, which daemons ignore, leaving new issues silently idle until
a user manually moved them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(issue): verify create defaults to todo, explicit backlog still works
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add per-agent custom_env configuration that gets injected into the agent
subprocess at launch time. This enables users to configure custom API
endpoints (ANTHROPIC_BASE_URL), API keys (ANTHROPIC_API_KEY), and cloud
provider modes (CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX) without
requiring code changes.
Changes:
- Migration 040: add custom_env JSONB column to agent table
- Backend: custom_env in agent CRUD API + claim endpoint
- Daemon: merge custom_env into subprocess environment variables
- Frontend: env var editor in agent settings (key-value pairs with
visibility toggle for sensitive values)
Closes#816
Related: #807, #809
Main introduced executeAndDrain/mergeUsage refactor. Resolve by keeping
main's refactored structure and re-applying EnvRoot to the switch/case
in runTask. Rename newTestDaemon → newGCTestDaemon to avoid collision
with the helper added in daemon_test.go on main.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The repoCache.Sync() call in loadWatchedWorkspaces runs synchronous git
clone/fetch operations that can take minutes for large repos. Because
heartbeatLoop and pollLoop only start after loadWatchedWorkspaces returns,
the runtime's last_seen_at is never updated during the sync, causing the
server's sweeper to mark it offline after 45 seconds.
Move repo cache sync to a background goroutine so heartbeat and poll
loops start immediately after runtime registration.
Closes#825
The context cancel in pruneRepoWorktrees was called explicitly after
CombinedOutput inside a loop. Extract to a helper method so defer
cancel() works correctly (scoped to the function, not the loop).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move WriteGCMeta from runTask() to handleTask() so it runs after
task completion, not at start. Mid-task crashes leave orphan dirs
that get cleaned by GCOrphanTTL.
- Strengthen isBareRepo to check both HEAD and objects/ directory.
- Remove empty workspace directories after all task dirs are cleaned.
- Add 30s context timeout to git worktree prune to prevent hangs.
- Add comprehensive unit tests for shouldCleanTaskDir (8 scenarios),
cleanTaskDir, gcWorkspace empty-dir cleanup, isBareRepo, and
WriteGCMeta/ReadGCMeta roundtrip.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(daemon): add fallback for failed session resume
When the daemon tries to resume a prior session (--resume flag for
Claude, --session for OpenCode, session/resume RPC for Hermes) and the
session no longer exists, the agent fails immediately. This adds a
fallback that retries the execution with a fresh session instead of
marking the task as blocked.
Extracts the execute+drain logic into a reusable executeAndDrain method
to avoid code duplication between the initial attempt and the retry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(daemon): narrow session resume retry and merge usage
Address review feedback:
1. Narrow retry trigger: only retry when result.SessionID == "" (no
session was established), not on any failure with PriorSessionID set
2. Merge token usage from both attempts so billing is accurate
3. Log errors when the retry itself fails to start
4. Add unit tests for mergeUsage, fallback behavior, and no-retry
when session was already established
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Isolation directories accumulate indefinitely because they're preserved
for session reuse but never cleaned up after the issue is closed.
This adds a background GC loop that periodically scans local workspace
directories and removes those whose issue is done/canceled and hasn't
been updated for 5 days (configurable via MULTICA_GC_TTL). Orphan
directories with no metadata are cleaned after 30 days.
Changes:
- Write .gc_meta.json (issue_id, workspace_id) at task completion
- Add GET /api/daemon/issues/{issueId}/gc-check endpoint for status queries
- Add gcLoop goroutine to daemon with configurable interval/TTL
- Prune stale git worktree references from bare repo caches each cycle
- New env vars: MULTICA_GC_ENABLED, MULTICA_GC_INTERVAL, MULTICA_GC_TTL,
MULTICA_GC_ORPHAN_TTL
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add online status indicator dot on agent & member avatars
Backend:
- Track member presence via WebSocket connections in the Hub
- Broadcast member:online/offline events when users connect/disconnect
- Add GET /api/workspaces/{id}/members/online endpoint
- Add member:online and member:offline event type constants
Frontend:
- Add isOnline prop to ActorAvatar with a status dot at top-right corner
- Green dot = online, gray dot = offline, no dot = status unknown
- Fetch online member list via new query, update optimistically on WS events
- Derive agent online status from existing agent.status field
- Wire online status through ActorAvatar views wrapper (enabled by default)
* fix: address code review — fix hub tests and avatar rounding
1. Hub tests: consume the member:online presence event from the first
connection before asserting on broadcast messages.
2. ActorAvatar: use rounded-[inherit] on the inner wrapper so callers
can override rounding (e.g. rounded-lg for agent list items).
* fix: consume member:online presence event in integration test
Same fix as the hub unit tests — read and discard the member:online
event before asserting on issue:created in TestWebSocketIntegration.
When a task is triggered by a comment, the agent prompt now includes
the comment content directly. This prevents the agent from ignoring
the comment when stale output files exist in a reused workdir.
Closes#805
Previously only Claude and Codex had log-scanning-level token usage
reporting (Flow B). This adds scanners for the remaining three runtimes:
- OpenCode: reads JSON message files from ~/.local/share/opencode/storage/message/
- OpenClaw: reads JSONL session files from ~/.openclaw/agents/*/sessions/
- Hermes: reads JSONL session files from ~/.hermes/sessions/
All three are registered in Scanner.Scan() and follow the same
(date, provider, model) aggregation pattern as existing scanners.
Adds CSP middleware to the global middleware chain as a browser-level
defense against XSS: script-src 'self', object-src 'none',
frame-ancestors 'none', base-uri 'self', form-action 'self'.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow agents to pipe comment content through stdin instead of the
--content flag, avoiding shell escaping issues with backticks, quotes,
and other special characters in markdown content.
Usage: cat <<'COMMENT' | multica issue comment add <id> --content-stdin
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(auth): migrate auth token to HttpOnly cookie & implement WebSocket Origin whitelist
Security improvements from the MUL-566 audit report:
1. Auth token is now set as an HttpOnly, SameSite=Lax cookie on login,
preventing XSS-based token theft. Cookie-based auth includes CSRF
protection via double-submit cookie pattern. The Authorization header
path is preserved for Electron desktop app and CLI/PAT clients.
2. WebSocket upgrader now validates the Origin header against a
configurable allowlist (ALLOWED_ORIGINS env var), rejecting
connections from unauthorized origins.
Backend: new auth cookie helpers, middleware reads cookie as fallback,
WS handler accepts cookie auth, Origin whitelist, logout endpoint.
Frontend: CSRF token in API headers, cookie-aware auth store and WS
client, web app opts into cookieAuth mode while desktop keeps tokens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(auth): address PR review — Strict cookies, HMAC-bound CSRF, origin sync
1. SameSite=Lax → SameSite=Strict per spec requirement
2. CSRF token now HMAC-signed with auth token (nonce.signature format),
preventing subdomain cookie injection attacks
3. allowedWSOrigins uses atomic.Value to eliminate data race
4. Removed magic "cookie" sentinel string in WSProvider — pass null token
and guard with boolean check instead
5. Removed dead delete uploadHeaders["Content-Type"] in API client
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
broadcastTaskDispatch was the only task event broadcast missing issue_id
in its WebSocket payload. The frontend task:dispatch handler had no way
to filter by issue, causing AgentLiveCard to briefly show activity for
the wrong issue when multiple tabs are open.
Closes https://github.com/multica-ai/multica/issues/791