ShardedStreamRelay shard readers previously started with lastID=,
which meant events published while a pod was down were silently lost.
Replace the cursor with a bounded time-window start ID derived
from (now - ReplayGrace), defaulting to 5 minutes. The timestamp is
clamped to 0 to handle misconfigured clocks gracefully.
Key changes:
- Add ReplayGrace field to ShardedStreamRelayConfig (default 5m)
- Add replayStartID() helper with non-negative clamp
- Extract readShardOnce() from readShard() for testability
- Add REALTIME_RELAY_REPLAY_GRACE env var for runtime tuning
- Add regression tests for bounded cursor and replay behavior
Closes#4797
The agent-task run-dedup keyed only on (issue_id, agent_id), so a
completed/pending verdict for commit A was silently reused to satisfy a
review request for a NEWER commit B pushed after A's run began — giving B
zero review coverage (nearly shipped an unreviewed commit; sibling of the
daemon disposition-loss bug in #4337).
Fix (no migration — reuses the existing context JSONB column):
- CreateAgentTask stamps the reviewed head_sha into the task's context.
- HasPendingTaskForIssueAndAgent(+ExcludingTriggerComment) now key dedup on
that head_sha: a pending task only dedups a request carrying the SAME
head. If HEAD advanced (or the pending task predates the stamp), dedup
MISSES and a fresh review enqueues. Empty head_sha (no linked PR) falls
back to the previous (issue_id, agent_id) key, so non-PR issues keep
coalescing unchanged.
- head_sha resolves from the issue's linked PR via GetIssueReviewHeadSha
(prefers open/draft, newest by pr_updated_at); ResolveIssueReviewSHA
fails soft to '' so a github-table hiccup can never over-dedup a review
out of existence.
- Threaded through all six dedup trigger sites (comment @mention + edit
preview, issue-status, squad-leader assign, child-done agent + squad).
Issue-linked tasks never reach quick-create context parsing, so the key
rides harmlessly alongside. Adds DB-backed regression tests pinning:
advanced-head misses dedup, repush invalidates dedup, same-SHA still
dedups, and no-linked-PR legacy fallback (verified non-vacuous against the
pre-fix query).
Co-authored-by: Multica Ops <multica-ops@tenanture.com>
claudeEffortSuperset treated a --help with no --effort line as 'help
format drifted' and fell back to the full level superset. On a host
whose claude binary predates the flag (e.g. 2.1.2), that let
ValidateThinkingLevel approve the agent's persisted thinking_level, so
the daemon injected --effort and the CLI exited 1 with
"error: unknown option '--effort'" — hard-failing every task instead
of degrading to a plain run. Field case: a stale root-owned
/usr/local/bin/claude shadowing a current install for daemons whose
PATH orders /usr/local/bin first (Multica desktop app), while a shell-
launched daemon on the same machine resolved a current claude — the
same agent then failed or succeeded depending on which daemon won the
task claim race.
Distinguish the two cases: --effort present but value list unparsed →
keep the full-superset drift fallback; --effort absent entirely →
return no levels, so the daemon's existing per-model guard drops the
level with a warning and the task runs without the flag.
Co-authored-by: Fable Chief Strategist <noreply@anthropic.com>
Adds an Electron `will-download` handler that forwards the filename Electron parsed from the server's `Content-Disposition` header (`item.getFilename()`) into the native save dialog, so desktop attachment downloads no longer default to `download.txt`.
Registration is guarded with a module-level `WeakSet<Electron.Session>` so the handler is installed at most once per session, even when macOS re-invokes `createWindow()` via `app.on("activate")`.
Fixes#4153
`go test ./...` compiles internal/handler and internal/scheduler into
separate binaries and runs them in parallel against the same DATABASE_URL.
Both mutate the global task_usage_hourly_rollup_state singleton (id=1) and
contend for the rollup function's advisory lock 4246, so under `-race` on CI
they interleave and fail flakily:
- TestRollupTaskUsageHourlyCapsWindowAtOneDay reads the scheduler test's
forced-back watermark (0.063 days ≈ the scheduler's now-90min) instead of
"now".
- TestPgCronConcurrentNoDoubleWrite sees a handler rollup tick advance the
watermark past its window, yielding winners=0.
Add a dedicated session-level advisory lock (42463980, distinct from the
function's own 4246) that every test touching the singleton acquires for its
duration, serialising them across test processes. Reproduced the exact CI
failures on a concurrent stress loop (5/5 rounds) and confirmed the guard
eliminates them (8/8 rounds green).
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): add v0.3.35 entry for the 2026-07-02 release (MUL-3984)
Summarize the changes on main since v0.3.34: the Issue-surface refactor and its
per-list cache/count accuracy, the chat live-timeline remount fix, four new
features (Show sub-issues toggle, add-label picker in the manual create dialog,
bulk 'Add to agent' on the skill detail page, and S3 path-style addressing
for self-host), plus this release's bug fixes.
Bullets stay in product language across en / zh / ja / ko. Only the Issue
concept is preserved untranslated in the Chinese entry; agent/Squad/sub-issue
follow the translated glossary.
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): tighten v0.3.35 bullets to one clause each (MUL-3984)
Per review, cut each bullet to a single short sentence and shorten the release
title. Same coverage across en / zh / ja / ko; content unchanged.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Connecting the same GitHub App installation in a second workspace silently
overwrote the first workspace's binding: github_installation was
UNIQUE(installation_id) and CreateGitHubInstallation's upsert overwrote
workspace_id on conflict (#4823).
Widen the uniqueness key to (workspace_id, installation_id) so each workspace
keeps its own binding row, and teach the webhook/lifecycle paths to handle N
bindings per installation_id:
- CreateGitHubInstallation upserts per (workspace_id, installation_id).
- Webhook lookup lists all bindings; PR/check_suite routing uses the oldest
binding as the deterministic fallback and still routes per-repo via the
existing workspace.repos registry.
- installation.deleted/suspend drops every workspace binding and broadcasts to
each affected workspace.
- installation.created/unsuspend refreshes account metadata across all bindings.
- Add a standalone index on installation_id (the dropped unique constraint was
the only index behind the webhook lookup).
MUL-3950
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): make agent access owner-only editable, read-only for others (MUL-3963)
Interaction bug: a non-owner (incl. workspace admin) could open the AccessPicker
and set an agent public — the backend silently ignored it and the UI bounced
back to private. Access is owner-only, so non-owners must see a read-only state
and the backend must reject real changes explicitly.
Frontend:
- AccessPicker renders a static, non-interactive read-only state when the
viewer is not the owner: the current access value + a lock affordance + a
tooltip "Only the agent owner can change who can run this agent." No clickable
trigger is rendered, so a non-owner can never open a control the backend would
reject (the GitHub/Notion pattern for permission settings you can see but not
edit). The editable multi-select picker is unchanged for the owner.
- agent-detail-inspector gates the picker on ownership specifically
(currentUserId === agent.owner_id), NOT the general canEdit (which also admits
admins, who may edit other fields but not access).
- New locale key access.owner_only_readonly (en/zh-Hans/ja/ko).
Backend:
- UpdateAgent now returns an explicit 403 when a non-owner submits a REAL
permission change (permissionInputChangesAgent compares requested mode +
target set against the persisted state); a no-op resubmit (admin PATCH-as-PUT
echoing unchanged permission) is still tolerated so admin edits of other
fields keep working. Replaces the previous silent-drop that caused the bounce.
Tests:
- access-picker.test.tsx: non-owner gets a non-interactive read-only display
with the owner-only tooltip; owner gets an interactive picker; owner can pick
a member and stack workspace + member.
- TestUpdateAgent_AccessChangeIsOwnerOnly: admin real change → 403; admin no-op
resubmit → 200; admin editing other fields → 200; owner change → 200.
Incidental: fixed a pre-existing base typecheck break in
slash-command-suggestion.test.tsx (stray `signal` arg not in the suggestion
items type) that otherwise fails the whole @multica/views typecheck.
Refs MUL-3963.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): compare legacy visibility, not expanded permission, for no-op detection (MUL-3963)
PR #4853 review: permissionInputChangesAgent expanded a legacy-only
visibility:"private" into a real private permission and compared it against the
agent's actual permission. A member-only public_to agent derives legacy
visibility "private", so an admin PATCH-as-PUT echoing visibility:"private"
while editing another field was misread as a public_to→private downgrade and
rejected with 403 — contradicting the "unchanged permission no-op is allowed"
contract.
Fix (per review): when a request carries ONLY legacy `visibility` (no
permission_mode / invocation_targets), derive the agent's CURRENT legacy
visibility from its real targets and compare the legacy string values. Equal =
no-op (allowed); a real legacy change (e.g. "workspace") still returns 403.
Requests that carry permission_mode / invocation_targets keep the precise
mode+target comparison.
Regression test TestUpdateAgent_LegacyVisibilityNoOpForMemberOnlyPublicTo:
member-only public_to agent — admin submitting visibility:"private" + a
non-permission field → 200 with targets unchanged; admin submitting
visibility:"workspace" → 403.
Go handler/composio suites green; migration 130 applied; go vet clean.
Refs MUL-3963.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Wire structured feedback kind through the frontend/core feedback path so desktop route-renderer errors submit as bug feedback, and bring the feedback client onto parseWithFallback. MUL-3768
* feat(agents): agent invocation permission system (permission_mode + invocation targets)
MUL-3963: split who may INVOKE an agent out of the overloaded visibility
column into an explicit, extensible model on feature/composio-integration.
- DB: agent.permission_mode (private|public_to) + agent_invocation_target
table (workspace/member/team targets) + lossless backfill from visibility
(migration 130).
- canInvokeAgent: owner-only for private (NO admin bypass, NO A2A bypass);
public_to honours the allow-list; A2A judged by the top-of-chain originator.
- All trigger paths rewired: issue assign, comment @agent/@squad, chat,
quick-create, autopilot, squad leader, child-done.
- Agent API: permission_mode + invocation_targets on responses and
create/update (owner-only writes); legacy visibility kept as a derived field
so old clients never see a permission widening.
- Composio: BuildTaskOverlay now FOLLOWS invocation permission and uses the
agent OWNER connection (removed the originator==owner gate); front-end warns
when a shared agent enables Composio apps.
- CLI: --permission-mode / --public-to-workspace / --public-to-member (legacy
--visibility still mapped).
- Frontend: AccessPicker (Private / workspace / specific people / team soon),
permission rules mirror canInvokeAgent, Composio warning banner.
- Tests: migration backfill, admin cannot invoke others private, public_to
workspace/member whitelist, A2A by originator, Composio overlay uses owner
connection.
Co-authored-by: multica-agent <github@multica.ai>
* feat(agents): stackable, mixed public_to invocation targets (MUL-3963)
Follow-up on PR #4844: public_to now supports selecting MULTIPLE, MIXED
targets on one agent (e.g. Public to workspace + specific people + team),
with canInvokeAgent admitting on ANY matching target (OR).
- Frontend AccessPicker: reworked from a single exclusive kind into a
stackable multi-select — an "Everyone in workspace" toggle, a member
multi-select checklist, and a (disabled, v1) team placeholder can be
combined freely. Emits the full union of selected targets; empty union
collapses to Private. Existing team targets are preserved across saves.
Added the access.public_group locale string (en/zh-Hans/ja/ko).
- Backend already supported this (agent_invocation_target is multi-row per
agent; create/update take a target ARRAY and batch-replace the whole
allow-list; canInvokeAgent OR-matches). Added tests to lock it in:
mixed member+team targets, overlapping-member batch replace, and
workspace+member stacking then narrowing.
Refs MUL-3963.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): address review on invocation permission (MUL-3963)
张大彪 review on PR #4844 — three blockers + product ruling + nits:
1. Migration 130: drop the FK/cascade on agent_invocation_target
(agent_id, created_by) per the Multica no-FK rule; relationships are now
maintained in the app layer (matching MUL-3515 §4). Added
DeleteAgentInvocationTargetsByArchivedRuntimeAgents and call it before
DeleteArchivedAgentsByRuntime in all three runtime-delete paths
(runtime.go x2, runtime_profile.go) so hard-deleting agents can't orphan
target rows.
2. revokeAndRemoveMember: prune the leaving member's member-target grants
(DeleteAgentInvocationTargetsByMember) in the same tx as the member-row
delete, so a re-invited user can't reclaim a stale invocation grant.
3. Empty public_to is a phantom — parsePermissionInput now normalises a
public_to with no resolvable targets to a single workspace target, so
`--permission-mode public_to` alone (and any empty target array) means
"public to workspace" instead of "shared but nobody can run it".
Product ruling: the system/no-human-originator → workspace-target path in
canInvokeAgent is a deliberate, documented exception (webhook/system/
workspace-wide automation); member/team targets still fail closed without a
resolved originator. Documented in code + locked with a test.
Nits: refreshed the stale "originator must be owner" comments — models.go
(via migration 130 COMMENT ON COLUMN + sqlc regen for composio_toolkit_allowlist
and originator_user_id) and agent-mcp-tab.tsx — to the owner-connection +
invocation-permission rules.
Tests: member remove/re-add regression, system workspace exception + member
fail-closed, empty public_to → workspace (plus the earlier mixed/overlap/
batch-replace suite). Migration 130 applied to the test DB; Go handler/service/
composio suites green; views typecheck clean.
Refs MUL-3963.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): scope member invocation-target cleanup to one workspace (MUL-3963)
张大彪 3rd review — cross-workspace permission bug + comment nits:
- DeleteAgentInvocationTargetsByMember was a GLOBAL delete by user id, so
removing a user from workspace A also wiped their member-target grants on
agents in workspace B. Scoped it to a single workspace by joining through
agent.workspace_id; revokeAndRemoveMember now passes (workspaceID, userID).
- Regression test TestRevokeMember_InvocationTargetCleanupIsWorkspaceScoped:
same user allow-listed by agents in two workspaces; removal from one leaves
the other workspace's target intact.
- Nits: refreshed the remaining stale "originator == agent.owner_id" /
"owner-vs-originator" comments — CreateRetryTask (agent.sql, regenerated),
and the AgentResponse allowlist doc + ListAgents/UpdateAgent redaction
rationale in agent.go — to the owner-connection + invocation-permission rule.
Migration 130 applied to the test DB; Go handler/service/composio suites green;
go vet clean.
Refs MUL-3963.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* 📝 [需求] 有序列表光标切换缺陷提案 / docs: requirements for 26-editor-ordered-list-cursor
Co-authored-by: multica-agent <github@multica.ai>
* fix(editor): keep the caret in the list item when the comment box remounts
Typing `1.` makes an ordered list, then switching issues and back yanked the
caret to the next line and re-yanked it there whenever the user moved it. The
comment box remounts on issue switch (key={id}) and feeds the persisted draft
back as defaultValue; the content-sync effect treated the redrawn draft as an
external change because @tiptap/markdown reserializes an ordered list slightly
differently from its source, re-parsed it, and restored the caret with a bare
Math.min offset clamp that resolved onto the list's structural (non-text) gap.
- Short-circuit the sync effect when the incoming parser INPUT matches what was
last applied (seeded at mount), so an identical draft is never re-parsed.
- Replace the Math.min clamp with clampSelectionToText, which snaps the prior
offsets to the nearest valid text position via TextSelection.between.
Tests: restore-selection unit tests (real editor) + a content-editor remount
regression test.
Co-authored-by: multica-agent <github@multica.ai>
* fix(editor): seed the caret guard synchronously and keep it in sync with the doc
Addresses cross-review findings on the content-sync guard:
- Seed appliedIncomingRef at render instead of in onCreate. Tiptap v3's
onCreate is deferred past the first sync-effect run, so the guard was null on
that run and a remount could still re-parse the ordered-list draft (the mock's
synchronous onCreate had hidden this).
- Advance appliedIncomingRef on the Guard 3 short-circuit too, so a later
external change back to an older value (e.g. a collaborator reverts the
description) is no longer mistaken for an already-applied input and swallowed.
Tests: add a suppressed-onCreate remount case and an A->B->A external-revert
regression; 23 passed.
Co-authored-by: multica-agent <github@multica.ai>
* docs: remove internal requirements proposal from the PR
Keep the PR to code and comments only, per reviewer request. The requirement
context lives in the tracking issue, not in this open-source code change.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): stop remounting the live timeline on every streamed task message (MUL-3960)
The Virtuoso components prop was built inline, so every render produced new
Header/Footer component types and React unmounted + remounted the entire live
timeline subtree. During task streaming that meant every task:message event
tore down and rebuilt thousands of rows and re-parsed all Markdown, freezing
the renderer for seconds at a time on long agent runs.
- Hoist Header/Footer to module scope and pass per-render data through
Virtuoso's context prop (re-render instead of remount).
- Memoize buildTimeline on the task-messages cache array identity (live
timeline and persisted AssistantMessage).
- Render message text through MemoizedMarkdown so unchanged content skips
the markdown re-parse; only the streaming tail re-renders.
Regression tests assert the footer DOM node survives a streamed message and
that a user-collapsed process fold stays closed (both failed before the fix).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* perf(chat): memoize MessageBubble so streamed messages skip untouched rows
Follow-up to the MUL-3960 remount fix: each task:message still re-rendered
every visible history row through itemContent. Message objects are
referentially stable and isPending is a boolean, so a shallow memo makes
the persisted history inert during streaming — only the live footer
reconciles per message.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Adjust the manual create-issue dialog toolbar so the add-label entry is
surfaced directly, and move the lower-frequency due date into the ⋯ menu:
- Add a Labels picker in the slot Due date used to occupy. LabelPicker
gains a draft mode (no issueId): selection is held via selectedIds /
onSelectedIdsChange and attached to the issue right after it's created
(the create endpoint takes no labels), mirroring the sub-issue linking
pattern already in this dialog.
- Collapse Due date into the ⋯ overflow menu with the same reveal rule as
Start date (inline only when it has a value or was just opened). Give
DueDatePicker the controlled open/onOpenChange props StartDatePicker
already had.
- Persist chosen labels in the issue draft store (labelIds) like every
other draft field.
- Add useAttachLabelToIssue (variables-based attach) so labels can be
attached to a just-created issue.
- i18n: add create_issue.set_due_date and toast_link_labels_failed across
en / zh-Hans / ja / ko.
MUL-3971
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): wake parent squad leader on same-squad/shared-leader child-done (MUL-3969)
The child-done stage-barrier wrote the 'Stage N complete / wrap up the
parent' system comment on the parent but suppressed the parent squad
leader's wake whenever the finished child was owned by the same squad
(childAssigneeIsSquad) or a squad sharing the leader (effectiveChildAgentOwner).
That stranded the common 'a squad decomposes its parent into sub-issues
it works itself' pattern: the parent silently stalled in in_progress
because the leader was never woken to advance the next stage or wrap up.
The prior guards assumed the leader had already observed the work via
its own coordination cycle on the child, but that wake lands on the
CHILD and never carries the parent-level stage-barrier instruction.
Remove both self-trigger guards so the squad path mirrors the agent path
(MUL-2808): always dispatch, bounded only by HasPendingTaskForIssueAndAgent.
The private-leader access gate is unchanged; member/unassigned parents
still never wake. Drop the now-dead effectiveChildAgentOwner /
childAssigneeIsSquad helpers and the unused child param.
Fixes#4838
Co-authored-by: multica-agent <github@multica.ai>
* test(issues): fix stale child-done squad-guard comment (MUL-3969)
The TestChildDoneTriggersParentAgentWhenChildSquadSharesLeader comment
still claimed the both-sides-squads-sharing-a-leader case was guarded on
the squad path and referenced the pre-rename test. That guard was removed
in MUL-3969; point at the renamed test and state the current behavior.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Treat MULTICA_DAEMON_PORT and a workdir daemon-task marker as daemon-managed signals so a task subprocess that loses MULTICA_TOKEN / MULTICA_AGENT_ID / MULTICA_TASK_ID fails closed instead of silently falling back to the user config-file PAT (which made agent writes land as the workspace owner). Adds an actionable error naming a leftover marker for local_directory recovery. Fixes#4204.
* MUL-3903 refactor project issue surface state
Co-authored-by: multica-agent <github@multica.ai>
* Refactor project issue surface ownership
Co-authored-by: multica-agent <github@multica.ai>
* Extract shared issue surface entrypoints
Co-authored-by: multica-agent <github@multica.ai>
* Fix issue surface create defaults and selection reset
Co-authored-by: multica-agent <github@multica.ai>
* test(editor): add missing AbortSignal to suggestion items() calls
The suggestion items() contract gained a required signal param; the
mention/slash test call sites were never updated, breaking pnpm typecheck
for @multica/views.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(issues): server-side assignee_types filter on ListIssues
ListGroupedIssues has taken assignee_types since squads shipped, but
ListIssues never did — so the workspace Members/Agents tabs had to fetch
the unfiltered workspace list and post-filter loaded pages client-side,
which made column totals and load-more pagination reflect the unfiltered
counts.
Add the same parse + WHERE clause to ListIssues (count query shares the
WHERE, so totals agree), thread the param through the TS client, and
widen MyIssuesFilter so scoped list caches can carry it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(issues): route issue cache writes through a membership-aware coordinator
useUpdateIssue, useBatchUpdateIssues, and the WS issue:updated handler
each maintained their own similar-but-diverging patch/invalidate rules.
Consolidate them into cache-coordinator.ts (applyIssueChange /
rollbackIssueChange / invalidateIssueDerivatives) so local writes and
remote echoes follow one rules table by construction.
The coordinator is membership-aware via surface/membership.ts
(true | false | unknown against each list cache's own filter contract):
- a change that moves an issue off a filtered surface removes the card
surgically (bucket total decremented) — fixes assignee changes leaving
stale cards on My Assigned with no local safety net (previously only
the WS echo recovered it), and replaces the blanket invalidate-myAll
net for project moves (MUL-3669) with per-key precision
- possible entry into a loaded list marks that key stale — never
hard-insert; page/slot is server knowledge
- stale keys flush on settle for mutations (a mid-flight refetch would
stomp the optimistic state) and immediately for WS
- batch updates now patch detail + inbox like single updates; the
off-screen bucket-count recovery previously exclusive to the WS path
now covers local mutations too
Preserved invariants: synchronous optimistic patches (dnd-kit), MUL-3375
control-field stripping, and no refetch of surgically reconciled lists
(the drag-flicker fix).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(issues): resolve surfaces via core query plan/repository with window-keyed remount
Read-path convergence and the loading/empty semantics that fall out of
it:
- scope -> API params moves from scope.ts helpers into
surface/query-plan.ts; workspace members/agents become server-filtered
scoped plans (assignee_types) and the client postFilter machinery is
deleted — tab counts and load-more are now exact
- query selection moves behind surface/repository.ts; the views data
hook no longer branches on workspace-vs-scoped plumbing
- IssueSurfaceContent remounts on data-window change (wsId + scope):
keepPreviousData placeholders keep sort/filter changes flicker-free
within one window but must never let project A's (or workspace A's)
cards impersonate B's with no loading state — cold window shows the
skeleton, warm window hits cache instantly
- isEmpty is only asserted from full-window data; the gantt
scheduled-only projection can't prove the window is empty, so GanttView's
own "no scheduled issues" empty state renders instead of the generic
create-issue one
- per-card project lookups hoist into a surface-level projectMap (drops
a per-card useQuery), create-defaults typing tightens to
IssueCreateDefaults
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(issues): count-only arithmetic for off-window status/membership changes
An issue beyond a list's loaded page window used to force a full
first-page refetch just to fix two column counts. When the change is
CERTAIN (base entity known, membership definitive) the coordinator now
does the arithmetic locally:
- stayed a member + status changed: move one unit of total between the
two buckets (loaded arrays untouched; hasMore stays consistent)
- left the list (reassigned / re-projected): old status bucket total -1
- member-to-member reassignment: counts unaffected, not even a stale key
Entering a list and any uncertainty (no base, unknown membership) still
refetch — the right page/slot is server knowledge. Branches on membership
OUTCOMES, not on which field changed, so future dimensions (team) join
automatically. Biggest win is the WS path: agents flipping off-screen
statuses no longer trigger refetch storms.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(issues): deferred view-refresh indicator during placeholder revalidation
Sort/date changes (and any grouped-board filter change) revalidate behind
the previous snapshot — correct, but on a slow network the click felt
dead: content stays put and isLoading never fires. Surface the state as
isRefreshing (isPlaceholderData of the active query) and render a shared
ViewRefreshIndicator in every issues header: a fixed-width slot (zero
layout shift) whose spinner fades in after 300ms, so sub-second responses
show nothing (NN/g) while slow ones get a working signal.
Bound to the revalidation STATE, not to any particular control — any
current or future server-side view change lights it automatically.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(markdown): render inline data-URI images (MUL-3961)
Inline data:image/* URIs (QR codes, charts, base64 screenshots) were
stripped and rendered as broken images. Two gates dropped the src:
- rehype-sanitize's protocols.src only allowed http/https
- react-markdown's defaultUrlTransform blanks any data: URL to ''
Allow data:image/* through both gates, narrowed to image subtypes only
(non-image data URIs stay rejected) and leaving every other src form
unchanged. file-cards.ts data: rejection is intentional and untouched.
Co-authored-by: multica-agent <github@multica.ai>
* fix(markdown): allow data:image/* in ReadonlyContent too (MUL-3961)
Issue comments and other read-only surfaces render through ReadonlyContent,
which keeps its own sanitize schema + urlTransform separate from the base
Markdown component. Both still stripped data: URIs, so an agent inlining an
auth QR code in an issue comment still saw a broken image.
Apply the same image/*-narrowed data: allowance (protocols.src +
attributes.img + urlTransform) and add a regression test covering the
comment / readonly path. file-cards.ts data: rejection stays untouched.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <agent-j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Emit the recovered Antigravity transcript reply as a MessageText event so the
execution transcript catches up with Result.Output on empty-stdout recovery.
Closes#4779
MUL-3941
* fix(slack): recover attachment/blocks text for empty-Text chat messages (MUL-3931)
`multica chat thread`/`chat history` dropped any message with an empty
top-level Text via normalizePage(), intended to skip join/system markers
but also discarding alerting/webhook bot roots (Grafana cards, incoming
webhooks) whose body lives in Attachments/Blocks. This regressed the
#4717 fix for its most common trigger: @mentioning the agent under an
alert card.
Add flattenSlackText(): fall back to Attachments[].Fallback, then
title/text/fields, then a best-effort Block Kit flatten (section/header/
context/markdown) before discarding. Derived text is capped so a verbose
card cannot flood the agent context; genuine content-less markers are
still dropped. Both read paths share normalizePage, so one fix covers
chat thread and chat history.
Closes#4803
Co-authored-by: multica-agent <github@multica.ai>
* fix(slack): flatten rich_text blocks in chat history fallback (MUL-3931)
Addresses the review nit on #4805: flattenBlocks() previously handled
section/header/context/markdown but skipped rich_text, so a message
whose only body is a rich_text block (the shape Slack rich-text input
produces) could still be dropped. Walk rich_text sections/lists/quotes/
preformatted runs and concatenate their text and link runs.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <agent-j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* fix(views): preflight mermaid syntax before rendering
* refactor(views): use suppressErrorRendering instead of a pre-render parse
Enable Mermaid's suppressErrorRendering so render() throws on invalid
syntax without drawing its built-in error graphic into the DOM, letting
the existing catch fall back to the compact error state. This drops the
redundant mermaid.parse() pass over valid charts while still fixing the
orphaned error-SVG artifact from #3653.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Surface the existing multi-select Add-to-agent dialog (previously only on
the skills list row/bulk actions) on each skill's own detail page, so one
skill can be attached to several agents at once. The trigger lives on the
"Used by N agents" sidebar panel header — the section that already lists
which agents have the skill — so adding more agents happens right there.
Agents that already have the skill render checked + disabled in the dialog
(the shared dialog's existing hasAll behavior). Also add an agent search
box to the shared AddToAgentDialog (benefits the list page too); groups
auto-expand while searching.
MUL-3922
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): add 'Show sub-issues' display toggle (MUL-3923)
Add a 'Show sub-issues' switch to the issue display-settings menu. When
turned off it hides sub-issues (issues with a parent) from the board,
list, swimlane and gantt views so users can focus on top-level / parent
issues. It is a pure display filter and never changes parent/child
relationships. Defaults to on and persists per view store, so /issues,
/my-issues, project detail and the actor tasks panel each remember it.
- view-store: new showSubIssues state + toggleShowSubIssues action,
persisted; propagates to my-issues and actor view stores via the shared
slice.
- filter: optional showSubIssues on IssueFilters; drop issues with a
parent when explicitly false (undefined keeps show-all, so existing
callers and mobile's positional variant are unaffected).
- wire the toggle into every surface that renders the display menu.
- i18n for en / zh-Hans / ja / ko.
- filter unit tests for the new toggle.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): apply Show sub-issues to assignee board & swimlane extra path (MUL-3923)
Address review of #4801 — two paths bypassed the new toggle:
1. Assignee-grouped board rendered straight from the server 'groups'
array, skipping the flat filterIssues() output, so sub-issues stayed
visible in assignee grouping on /issues, /my-issues and project detail.
Added filterAssigneeGroups() which re-applies the client-only display
filters (Show sub-issues + the agents-working quick filter) to each
group, recomputes total and drops emptied groups. Wired into all three
surfaces. Generalizes and replaces the old filterRunningAssigneeGroups.
2. Parent swimlane's batch/per-parent extra-children merge rebuilt its
internal activeFilters without showSubIssues, so lazily-loaded
sub-issues reappeared even with the toggle off. Carry showSubIssues
through.
Tests: filterAssigneeGroups unit tests (sub-issue hide, running filter,
AND composition, empty-group drop, by-reference passthrough) and a
swimlane test covering the batch-children path with the toggle off.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The Slack `/issue` slash command used to directly create a raw issue: the
typed line became the title verbatim and a `todo` issue was assigned to the
agent to work on immediately. That files a rough, unstructured issue and starts
the agent on it before it is well-formed.
Switch the slash command to the quick-create pipeline instead
(TaskService.EnqueueQuickCreateTask, the same path as the web "quick create"
modal): the invoker's natural-language description is handed to the
installation's agent as a prompt, and the agent authors a well-formed issue
(proper title + structured description) in the background, attributed to the
bound member. Because creation is now asynchronous, the ephemeral reply is an
acknowledgement ("On it…") rather than a created-confirmation with a number;
the agent's completion surfaces to the invoker as a Multica inbox notification
through the shared quick-create completion path.
Installation routing and identity/membership checks are unchanged, so the same
workspace boundary and account-binding rules apply. Scope is the slash command
only — the message-based `@bot /issue` still runs through the shared
cross-platform engine (which also serves Lark) and keeps its direct-create
behavior.
- slash_command.go: swap IssueService.Create for EnqueueQuickCreateTask via a
narrow quickCreateEnqueuer interface; prompt is the full text (no title/body
split); drop the now-unused splitIssueText / issueCreatedText / GetWorkspace.
- router.go: wire h.TaskService instead of h.IssueService.
- tests: cover enqueue + ack, multiline prompt pass-through, empty prompt,
unbound, non-member, inactive, team mismatch, and enqueue-failure.
- docs (4 locales): describe the quick-create behavior.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
A Slack user who linked their Multica identity to one bot was re-prompted
to link again when messaging a second bot (a different Slack app) in the
SAME Slack workspace, because bindings are keyed per-installation
(installation_id, channel_user_id).
On an unbound inbound, the identity resolver now looks up an existing
binding for the same (Multica workspace, Slack team, Slack user) via the
new FindReusableChannelUserBinding query and, if that user is still a
workspace member, materializes a binding for the new installation instead
of returning ErrSenderUnbound — so the second bot resolves the user
silently. Reuse is fenced to one Multica workspace AND one Slack team, so
it never crosses either boundary; legacy installs with no recorded team
never reuse.
Adds resolver unit tests for every decision path and updates the Slack
integration docs (en/zh/ja/ko).
MUL-3911
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Follow-up to #4780 (the /issue slash command), which merged the code + docs. The DB migration was pushed to that branch after it had already been squash-merged, so it never landed on main — the /issue command still fails at issue creation.
The issue.origin_type CHECK constraint (migration 111) only allowed autopilot / quick_create / lark_chat. Slack stamps origin_type='slack_chat' on every /issue create, so the INSERT trips SQLSTATE 23514 and IssueService.Create fails ("Something went wrong creating the issue"). This also silently broke the pre-existing message-based Slack /issue. Extends the constraint to include 'slack_chat', mirroring 111 (lark_chat). Numbered 131 because 129/130 were taken on main since #4780 branched.
Verified against a real Postgres: full chain up to 131 applies cleanly and an issue insert with origin_type='slack_chat' passes the CHECK after 131 (fails with the pre-131 constraint).
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(slack): native /issue slash command over Socket Mode (MUL-3908)
A message beginning with `/issue` is intercepted by the Slack client as a slash command and never delivered to the app, so the message-prefix /issue never worked on Slack (no event, no 👀, no issue).
Register /issue as a real slash command in the app manifest and handle EventTypeSlashCommand over the existing per-installation Socket Mode connection. It is a one-shot issue creation (no chat session / agent run) that reuses the shared IssueService and the same installation-routing + identity/membership checks as the message path, replying privately via the command's response_url (ephemeral) since a slash command has no message to react to.
Docs: register the command in the manifest and describe the slash-command behavior across all four locales.
Co-authored-by: multica-agent <github@multica.ai>
* docs(slack): add commands scope to /issue manifest; fix chat-run wording (MUL-3908)
Review follow-up: the manifest examples registered features.slash_commands but omitted the commands bot scope, so updating + reinstalling could still fail to grant the /issue command. Add - commands to oauth_config.scopes.bot in all four locales and document it in the permissions table.
Also correct the misleading "no agent run" wording in the slash-command header and router comment: a todo issue assigned to the agent still triggers it via maybeEnqueueOnAssign (issue-assignment), like the message /issue — the slash command only skips the chat session / chat run.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>