mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 06:28:23 +02:00
fix/transcript-virtual-scroll
4309 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
359ef61dc3 |
fix(search): pg_trgm index fallback + statement_timeout guard (MUL-4059) (#4925)
* fix(search): add pg_trgm index fallback + statement_timeout guard (MUL-4059) Root cause of the "search freezes with no response" symptom reported in MUL-4059: the search handler runs LOWER(col) LIKE '%pattern%' queries that expect a pg_bigm GIN index (migrations 032, 033, 036), but every migration wraps the CREATE EXTENSION + CREATE INDEX in a DO/EXCEPTION handler that silently skips when pg_bigm is unavailable. The bundled self-host / dev / CI Postgres image (pgvector/pgvector:pg17) does not ship pg_bigm, so on every self-hosted deployment the migrations no-op and no GIN indexes get built. Every /api/issues/search + /api/projects/search request then falls back to a Seq Scan on `issue` + correlated Seq Scans on `comment` — verified with EXPLAIN on the local dev DB, which has zero title/description/comment search indexes before this change. Two independent guardrails are added, either of which alone would have prevented the reported hang: 1) Migration 134 installs pg_trgm (ships in all standard Postgres + pgvector images) and builds GIN indexes with gin_trgm_ops on `LOWER(title)`, `LOWER(COALESCE(description, ''))`, and `LOWER(content)`. The expression signatures match the search handler's WHERE clauses exactly, so the planner picks the index without further changes. The pg_bigm indexes from 036 are left intact — deployments on AWS RDS with pg_bigm 1.2 keep the CJK-friendly bigram path; deployments without it get the trigram fallback. Verified against a local 25k-row fixture: the description LIKE hits `Bitmap Index Scan on idx_issue_description_trgm` in 0.5 ms. 2) runSearchQuery wraps both search handlers in a short-lived read-only transaction with SET LOCAL statement_timeout = 3 s. In the pathological case where indexes are still missing or the query plan is bad, callers see a fast 503 with a descriptive error instead of a stalled request. Verified against a live Postgres: a deliberate pg_sleep(2) with the test override at 200 ms is cut off in 230 ms with SQLSTATE 57014, as asserted by TestRunSearchQuery_StatementTimeoutFires. Non-goals: this change does not remove the pg_bigm code path, does not change the SQL the handler builds, and does not change the API response shape. It is the minimum diff to unblock production while preserving the CJK-search advantage that pg_bigm provides where it is available. Co-authored-by: multica-agent <github@multica.ai> * fix(search): scope comment subqueries to workspace to unblock prd hang (MUL-4059) Follow-up correction after PRD investigation: pg_bigm IS installed on prd `multica-prod` and all five bigm indexes exist in the correct `LOWER(...) gin_bigm_ops` form. The initial "missing index" hypothesis was wrong; migration 134 (pg_trgm fallback) still helps self-host but does not touch the production hang path. Actual prd EXPLAIN (workspace with 60k issues, keyword "search"): Index Scan using idx_issue_workspace on issue i Rows Removed by Filter: 59123 SubPlan 2 Bitmap Heap Scan on comment c Rows Removed by Index Recheck: 1928275 Heap Blocks: exact=48297 lossy=164696 Bitmap Index Scan on idx_comment_content_bigm rows=536761 Execution Time: 32345.002 ms Root cause: the correlated `EXISTS` over `comment` gets rewritten by the planner into a *hashed* subplan. Without a workspace_id filter in the subquery, that hashed set covers every comment in every workspace matching the LIKE — 536k rows for "search" — which spills work_mem into a lossy bitmap and rechecks 1.9M rows. Two-part fix: 1. Query rewrite. buildSearchQuery now emits `c.workspace_id = $wsParam` inside every comment subquery (WHERE phrase match, WHERE multi-term match, tier 7 rank, tier 8 rank, and the matched_comment_content COALESCE). The same $4 parameter is reused so Postgres treats it as a compile-time constant and pushes it into the hashed subplan's key, collapsing the set to this workspace's comments. 2. Supporting index (migration 135). New `idx_comment_workspace ON comment (workspace_id)`. Without it, the pushed-down filter still triggers a Seq Scan on `comment` because comment has no btree on workspace_id (only the FK constraint and composite (issue_id, ...) indexes). Locally verified against a repro that mirrors prd (5k issues in the target workspace, 100k comments in a sibling workspace all containing "search"): the plan drops from 60 ms (hashed global scan, no support index) to 3 ms (subplan uses idx_comment_workspace). Prd extrapolation from the same shape: 32.3 s → tens of milliseconds. Regression test TestBuildSearchQuery_CommentSubqueryWorkspaceScope asserts every `FROM comment c` in the generated SQL is followed by a `c.workspace_id = $4` filter, so a future refactor can't silently regress the plan back to the global-hash pathology. The statement_timeout guard from the earlier commit in this branch is kept — it still bounds the worst case if any future query shape regresses. Co-authored-by: multica-agent <github@multica.ai> * fix(search): address PR review — unwrap 135 + add project trigram indexes (MUL-4059) Both must-fix items from GPT-Boy's review: 1. Migration 135 unwrapped. The previous version buried `CREATE INDEX idx_comment_workspace` inside `DO $$ ... EXCEPTION WHEN OTHERS $$` — exactly the anti-pattern that caused MUL-4059 in the first place. `idx_comment_workspace` is not a CJK-bonus fallback; it is the critical support that makes the query rewrite land on an Index Scan instead of a Seq Scan. A silent failure (lock timeout, disk full, permission denied, schema drift) MUST abort the migration and fail deployment, not slip through as green. The unwrapped `CREATE INDEX IF NOT EXISTS` now propagates real errors to the migration runner, which aborts and does NOT record the version as applied. IF NOT EXISTS keeps idempotency for the operator-precreated case (`CREATE INDEX CONCURRENTLY ...` before running migrations on large prd tables). 2. Migration 134 now covers project search too. SearchProjects reads `LOWER(project.title)` and `LOWER(COALESCE(project.description, ''))`, and the pg_bigm equivalents in migration 039 silently no-op on pg_bigm-less images just like 032/033/036. Without the trigram fallback, project searches on self-host would still Seq Scan and hit the 3 s statement_timeout guard as a 503 — technically bounded but not actually fixed. Added `idx_project_title_trgm` and `idx_project_description_trgm`; the down migration drops them too. Also: fixed the search.go comment that said callers get a "standard 500" — they get a 503 with SQLSTATE-57014 mapping; the comment now matches reality. Verified: build clean, vet clean, existing search / timeout tests still green. Migration 135 dry-run (dropping the index, re-applying the unwrapped SQL under `ON_ERROR_STOP=1`) creates the index cleanly; a deliberate `CREATE INDEX` on a non-existent column now aborts psql with exit 3, confirming the migration runner would fail loudly on any real error. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c84a939c83 |
fix(views): enable multi-file selection on all attachment upload buttons (#4962)
The FileUploadButton component already fans out per-file onSelect callbacks and every editor surface already handles N concurrent uploads (drag-drop and paste were multi-file all along), but five call sites never passed the `multiple` prop, so the OS file dialog capped picks at one file: chat composer, create-issue modal, quick-create modal, issue description, and feedback modal. Fixes MUL-4074. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ebd0248be2 |
MUL-4016: fix mention tokenizer stacktrace backtracking (#4889)
* MUL-4016: fix mention tokenizer stacktrace backtracking Co-authored-by: multica-agent <github@multica.ai> * fix(editor): de-ambiguate escaped-label regexes to kill ReDoS (MUL-4016) The mention/slash/file-card label regexes used `(?:\\.|[^\]])` where both alternatives can consume a backslash. On an unterminated match, each `\x` run is enumerated 2^n ways — pasting a Java stacktrace (`\~\[...\]`) or a crafted ~50-char string freezes the main thread for seconds (GitHub #4881). Exclude backslash from the char class (`[^\]\\]`) so a backslash can only be consumed by `\\.`. The alternatives become disjoint and matching is linear; legal escaped-bracket labels like `David\[TF\]` still parse unchanged. Fixed in all four sites that shared the pattern: - mention-extension.ts tokenize() - slash-command-extension.ts start() + tokenize() - file-card.tsx FILE_CARD_MARKDOWN_RE - packages/ui/markdown/file-cards.ts NEW_FILE_CARD_RE (runs on every read-only comment/description render, not just the editor) Adds adversarial regression tests (repeated `\a` + missing closing bracket) that fail in ~10-40s against the old regexes and pass in <1ms after the fix. Builds on #4889's marker-first mention start(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(editor): escape backslash in mention/slash labels for round-trip (MUL-4016) Follow-up to the de-ambiguation fix, addressing Howard's PR review. The linear tokenizer now treats "\" as an escape lead (\\.), so a label whose serialized form contains a bare "\" adjacent to the closing "]" no longer parses back — the "\]" is consumed as an escaped bracket and swallows the boundary. The old ambiguous regex tolerated this by chance; the de-ambiguation exposes it. mention/slash renderMarkdown escaped only [ and ], not \. Switch both to the shared escapeMarkdownLabel() (escapes [ ] \ ( )) and mirror it on parse with replace(/\\([[\]\\()])/g, "$1"), matching what file-card already does. This also converges the three tokenizers on one escape contract. file-card was already correct and is unchanged. Adds parameterized round-trip tests for labels containing "\" / "\]" / parens (e.g. "A\\", "ends\\", "a\\]b"); these fail on the old serializer and pass now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7116691c07 |
fix(github): hide reference-only PR links from the issue PR list (#4611)
* fix(github): hide reference-only PR links from the issue PR list A PR that merely mentions an issue key in passing in its description (e.g. "Related to MUL-3739") was auto-linked and shown in that issue's right-side PR list as if it were a working PR for the issue. Add a reference_only flag to issue_pull_request. The webhook keeps linking generously (so close_intent stays trackable across edits) but flags a link as reference_only unless the key is a genuine target: a title prefix, a branch reference, or a body closing keyword (Closes/Fixes/Resolves). ListPullRequestsByIssue filters reference_only rows, so passing body mentions are hidden from the CLI and the UI PR list while real targets remain. reference_only follows the same terminal preserve gate as close_intent; the auto-advance gate is unchanged. Closes MUL-3739 Co-authored-by: multica-agent <github@multica.ai> * fix(github): exclude reference_only links from the close aggregate A reference_only link is hidden from the issue PR list, but GetIssuePullRequestCloseAggregate still counted it toward open_count. An open body-only mention ("Related to MUL-X") could therefore block the issue from auto-advancing to `done` after a real closing PR merged, while being invisible in the right-side PR list. Filter `AND NOT reference_only` in the aggregate too (reference_only rows never carry close_intent, so merged_with_close_intent_count is unchanged). Add TestWebhook_HiddenBodyMentionDoesNotBlockAutoAdvance. Addresses code review on PR #4611. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5901997bf6 |
fix(squad): wake private-leader squad parent leader on child-done (MUL-4063) (#4934)
* fix(squad): wake private-leader squad parent leader on child-done The child-done parent wake routed squad leaders through canEnqueueSquadLeader/canInvokeAgent, while the agent-parent path (triggerChildDoneAgent) has never gated. Agents default to private visibility, so a default squad leader is private; when a child is closed by an agent/system actor (the normal process-squad pipeline) there is no resolvable human originator, the gate fails closed, and the leader is never woken -- stranding every multi-stage squad pipeline after its first stage. Assigning the parent directly to the leader agent worked only because that path is ungated. Remove the child-done leader-invocation gate so agent and squad child-done follow one path. The parent was already permission-checked at squad-assign time (validateAssigneePair); waking its own leader to advance the next stage is a coordination handoff, not a fresh invocation, and grants no new privilege -- the actor can only wake the leader on the specific parent that leader already owns. If invocation permission is ever reintroduced it must be added to both paths together. Also drops the now-dead actor plumbing threaded solely for the gate, flips the plain-member child-done test to assert the leader is woken, adds an agent-actor regression, and updates the squad / mentioning skill docs. MUL-4063 Co-authored-by: multica-agent <github@multica.ai> * docs(squad): refresh Private Leader Access source map to canInvokeAgent The squad + mentioning source maps still described the old canAccessPrivateAgent model (visibility!=private, agent short-circuit, system->agent remap). The trigger gate is canInvokeAgent (MUL-3963); update both to match and note the child-done wake is now ungated (MUL-4063). Review nit follow-up, docs only. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cc1f5cda8a |
fix(issues): don't call an intermediate stage final in child-done comment (MUL-4062) (#4932)
The staged child-done system comment derived its "final stage vs next stage" wording from stageProgressSummary over the sub-issues that currently exist. The server has no declarative workflow model — stages are agent-driven and often created lazily (stage N+1's sub-issues are written only after stage N produces the inputs they depend on), so an intermediate stage reaches nextStage==0 exactly like a true final stage. The old else branch then asserted "This was the final stage. Wrap up the parent", pushing leaders/humans to wrap up mid-workflow (GH #4927). Extract the trailing instruction into stageAdvanceInstruction and, when no later stage exists among the created sub-issues, stop asserting finality: name both possibilities (create the next stage, or wrap up) and hand the decision back to the leader. Add a unit test locking in that the nextStage==0 message never claims a definitive final stage. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
129efb7688 |
feat(runtime): allow qoder as a custom runtime profile base (#4883) (#4912)
Qoder CN (`qoderclicn`) users could only reach a working custom runtime by misrouting through the Kiro backend, which launches `<cmd> acp --trust-all-tools`. That is incompatible with Qoder's global `--acp` / `--yolo` argv, so the task failed immediately with `kiro initialize failed` and no run messages. Expose `qoder` in the custom-profile protocol_family whitelist across every lockstep layer: - server/pkg/agent SupportedTypes (+ whitelist pin test) - migration 134 runtime_profile protocol_family CHECK (NOT VALID, mirroring 126) - packages/core RUNTIME_PROFILE_PROTOCOL_FAMILIES The existing qoderBackend already honors an ExecutablePath override and launches `<cmd> --yolo --acp`, so a profile with protocol_family=qoder and command_name=qoderclicn now launches with the correct argv instead of the Kiro shape. Provider branding/logo for qoder already exists. MUL-4018 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1ae0a1f5bf |
fix(agents): defend AccessPicker against undefined invocation_targets (GH #4915) (#4924)
Opening the agent detail page in v0.3.37 crashed the whole route with "Cannot read properties of undefined (reading 'some')" whenever the cached agent record's `invocation_targets` was missing at runtime — even though the TypeScript type declares it a required `AgentInvocationTarget[]`. Root cause: `AccessPicker`'s `hasWorkspaceTarget` / `selectedMemberIds` / `selectedTeamIds` helpers called `.some()` / `.filter()` directly on the prop, and the same unguarded pattern was mirrored in `AgentMcpTab` and the `canAssignAgentToIssue` permission gate. The field can legitimately be undefined at runtime because: - `packages/core/api/schemas.ts` declares `invocation_targets` as `.optional()`, and `MinimalAgentSchema` (used for the create-from- template response) also marks it optional. - `api.listAgents` / `api.getAgent` return raw JSON without running through the schema, so an older self-host backend that predates MUL-3963 (permission_mode + invocation_targets) — the exact scenario in GH #4915 — yields an Agent with the field missing. Fix (`?? []`) at every site that indexes into the list, matching the already-established pattern in `create-agent-dialog.tsx`: - `access-picker.tsx`: coerce inside the three helpers and widen the prop type to `AgentInvocationTarget[] | undefined` so the accepted runtime shape is documented. - `agent-mcp-tab.tsx`: `(agent.invocation_targets ?? []).some(...)`. - `permissions/rules.ts`: `agent.invocation_targets ?? []` before the workspace / member membership checks. Adds three regression tests (AccessPicker owner + read-only paths, AgentMcpTab shared-warning path, canAssignAgentToIssue) that pass `undefined` for invocation_targets and assert the UI degrades to the private / empty-allowlist state instead of throwing. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>v0.3.38 |
||
|
|
1ff99e5afc |
fix: honor completed codex turns during process eof races (#4899)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>v0.3.37 |
||
|
|
d90ee9fa35 |
fix(agents): thread permission_mode/invocation_targets through the template create path (MUL-4010) (#4897)
CreateAgentFromTemplate accepted only the legacy visibility field and dropped
it on the floor: neither permission_mode nor invocation_targets flowed into
the INSERT, so the SQL default (COALESCE(sqlc.narg('permission_mode'),
'private')) pinned every template-created agent as private in the new
invocation-permission model (MUL-3963). Since canInvokeAgent reads
permission_mode — not the legacy visibility column — a request that asked
for a workspace-shared agent (old Web/CLI/Desktop sending
visibility="workspace", or new Web sending permission_mode/public_to +
invocation_targets) silently landed as owner-only. The public_to+targets
inputs from the new Web front-end were also being ignored.
Fix (mirrors handler/agent.go:CreateAgent so the two entry points can't
drift):
- CreateAgentFromTemplateRequest gains PermissionMode *string and
InvocationTargets []AgentInvocationTargetDTO.
- Decode via decodeJSONBodyWithRawFields to distinguish an absent
invocation_targets from an empty one (same rawFields lookup CreateAgent
uses).
- Call parsePermissionInput(wsUUID, req.PermissionMode,
req.InvocationTargets, req.PermissionMode != nil, hasTargets,
&legacyVis) so the legacy 'workspace' mapping ('workspace' -> public_to +
workspace target) is applied uniformly.
- Pass perm.legacyVisibility() into Visibility and perm.mode into
PermissionMode on CreateAgentParams so the visibility mirror column stays
aligned and the permission_mode column reflects the caller's intent
rather than the SQL default.
- Persist the invocation allow-list inside the same tx as the agent row via
a new tx-friendly helper replaceInvocationTargetsWithQueries — an agent
is never observable in a state where the row exists but its targets are
missing. handler-level replaceInvocationTargets delegates to it with
h.Queries, keeping the CreateAgent/UpdateAgent call sites unchanged.
- Enrich the response with invocation targets after commit so a client that
just asked for visibility='workspace' sees the derived legacy visibility
round-trip correctly (previously the response echoed empty
invocation_targets and legacy 'private' regardless of intent).
Regression coverage in agent_template_permission_test.go:
- TestCreateAgentFromTemplate_LegacyVisibilityMapsToPermission: both
legacy visibility values are exercised. workspace -> permission_mode
public_to + a workspace invocation-target row (row-level SELECTs assert
the persistence, not just the response echo); private -> permission_mode
private + zero target rows.
- TestCreateAgentFromTemplate_PublicToWithMemberTarget: new-shape request
(permission_mode='public_to' + a member invocation-target) is honoured
verbatim, derived legacy visibility collapses to 'private' (member-only
public_to), and the DB row for the member target exists.
Uses commit-message as the fixture template (zero external skills), so the
tests don't need to reach any network fetcher.
Co-authored-by: multica-agent <github@multica.ai>
v0.3.36
|
||
|
|
022c8a63d0 |
docs(changelog): add v0.3.36 entry for the 2026-07-03 release (#4893)
* docs(changelog): add v0.3.36 entry for the 2026-07-03 release Summarize the changes on main since v0.3.35: the Composio MCP apps rollout (server-side connect flow, per-agent allowlist, and the new Private / public-to invocation permission model — all behind the composio_mcp_apps feature flag), transcript filter/expansion memory, and Helm support for an externally managed PostgreSQL. Bug fixes: empty ordered-list caret repair, daemon agent CLI discovery through login-shell hook wrappers, per-commit-SHA reviewer-loop dedup, bounded ShardedStreamRelay replay window, Kiro ACP usage accounting, autopilot create_issue visibility while runtime is offline, Slack attachment-body preference, Codex model catalog in task home, legacy /squads and /usage redirects, correct filename in the desktop save dialog, private squad-leader wake from HTTP-authored worker comments, Composio Settings hides toolkits with no auth config, and a graceful fallback when the host Claude CLI predates --effort. Bullets stay in product language across en / zh / ja / ko. Only the Issue concept is preserved untranslated in the Chinese entry; agent, Squad, and sub-issue follow the translated glossary. Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): drop Composio bullets — still behind feature flag Per Yushen: the Composio app connections feature (and the two Composio- gated polish bullets — Settings toolkit filtering and the create-agent access picker) is still gated by composio_mcp_apps and is not user- visible in this release. Removing those bullets from all four locales (en / zh / ja / ko) and retitling the v0.3.36 entry around the two remaining user-visible features (transcript view memory, Helm external PostgreSQL) plus the bug-fix list. Structure and every other bullet unchanged. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
910bbe9309 |
MUL-4024 tighten squad leader self-trigger guard (#4896)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
02a845b31c |
chore: add trailing newline to README (MUL-4026) (#4890)
No-op whitespace change to exercise the manual-merge test flow requested in MUL-4026. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
dd9996d0aa |
MUL-4014: persist transcript filters and expansion (#4884)
* feat(transcript): persist log view preferences Co-authored-by: multica-agent <github@multica.ai> * fix(transcript): wrap modal header controls Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4d968ba875 |
fix(agents): gate create-agent access picker on composio_mcp_apps flag (MUL-4010) (#4888)
Reuse the existing `composio_mcp_apps` feature flag instead of the separate `agent_access_picker` flag introduced in #4879. The MUL-3963 permission_mode + invocation_targets model exists to gate Composio sharing, so the create-flow access picker ships on the same switch as the rest of the Composio rollout — environments that already enable Composio (`FF_COMPOSIO_MCP_APPS=true`) now also see the aligned Private / Public-to picker in Create / Duplicate. - Drop `AGENT_ACCESS_PICKER_FLAG` (frontend keys.ts + index re-export). - Drop `AgentAccessPicker` (server featureflags list). - `CreateAgentDialog` reads `COMPOSIO_MCP_APPS_FLAG` instead. - Tests updated to set the composio flag. All 10 create-agent-dialog tests + Composio-related tabs tests pass. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
098b1f6362 |
MUL-4015: stamp source_task_id on HTTP-authored agent comments (#4886)
The HTTP CreateComment handler read X-Task-ID for parent-id validation and the no_action gate, but never stamped source_task_id on the comment row. That silently broke the originator inheritance chain used by every task the comment triggers downstream (resolveOriginatorFromTriggerComment climbs comment.source_task_id → parent task's originator_user_id). Consequence: on a squad-assigned issue where the leader agent is private (the default permission_mode for agents), the leader → worker mention hop would enqueue the worker's task with originator_user_id = NULL. When the worker later posts its result comment, invokeOriginatorFromRequest reads that NULL back out, opts.OriginatorUserID becomes "", and routeAssignedSquadLeaderFallback → canInvokeAgent denies the leader wake (private agents admit only the owner, and effectiveUser is empty). The leader → worker → leader coordination loop stayed broken until the leader was triggered by something else. Public-to-workspace leaders papered over the issue via the workspaceBroad admittance path in canInvokeAgent. Fix: capture the same X-Task-ID the handler already reads and pass it as SourceTaskID on the CreateComment call. Only stamp when the task belongs to this issue — a same-agent, different-issue comment must not attribute itself to an unrelated task's originator. All existing gates (parent_id-vs-trigger_comment mismatch, no_action) still fire before the stamp is applied. Regression coverage in squad_worker_comment_wakes_leader_test.go: - worker-agent completion comment wakes a public_to squad leader (baseline: was passing before the fix, kept as guardrail) - worker-agent completion coalesces when a leader task is already queued - worker-agent completion wakes a PRIVATE squad leader through the leader → worker mention hop (the failing case, red before → green after) Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
77ba0fdddb |
MUL-4009: hide not-configured Composio toolkits at Service layer (#4880)
* MUL-4009: hide not-configured Composio toolkits at Service layer Filter out toolkits with no enabled auth config in Service.ListToolkits so the Settings UI only shows connectable apps. A dead 'Not configured' card is noise for end users, so drop the entry entirely instead of showing a greyed label (which existed only to avoid a dead Connect button, MUL-3720). - service.go: only append connectable toolkits; drop the connectable-first sort (all entries are connectable now); a resolver error now returns an error (502) instead of masking to an empty catalog, so the UI shows its honest load-failed state rather than a misleading 'no apps configured'. - Keep the wire 'connectable' field (always true) for backward compat with older desktop clients that branch on it. - composio-tab.tsx: remove the 'Not configured' branch; keep the toolkit.connectable guard as a client-side backstop. - i18n: drop composio.not_connectable / not_connectable_hint from en/ja/ko/zh-Hans. - Update service + handler tests to assert filtering and the resolver-error path. Co-authored-by: multica-agent <github@multica.ai> * MUL-4009: address review — empty-state copy, 502 handler test, stale comments Review follow-up on #4880: - i18n: rewrite composio.page_description / empty_title / empty_description in all 4 locales. The empty state now means 'no toolkit with an enabled auth config in the project', not 'Composio returned no catalog' — the old copy misled users after filtering landed. - Add TestComposio_ListToolkits_ResolverErrorIs502: fakes an auth-config resolver error and asserts ListComposioToolkits returns 502, pinning the no-silent-empty-catalog behavior (composioFakeSDK gains listAuthErr). - Refresh stale 'full catalog / false connectable' docs in packages/core/types/composio.ts, composio/queries.ts, api/client.ts to the connectable-only model. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b24b63c513 |
feat(agents): align create-agent visibility with MUL-3963 access model (MUL-4010) (#4879)
Adopt the private / public_to invocation-permission model in the Create Agent (and Duplicate) dialog to match the AccessPicker on the agent detail page. When the new `agent_access_picker` feature flag is ON, the visibility section is replaced with an inline access editor that: - Toggles between Private (owner-only) and Public - Under Public, offers "Everyone in workspace" plus a member allow-list (multi-select, current user excluded, mirroring AccessPicker semantics) - Preserves team targets on the source agent when duplicating - Collapses an empty public_to (no workspace, no members) back to private on submit — same normalisation the AccessPicker emits The dialog submits `permission_mode` + `invocation_targets` in that mode, matching the authoritative gate; when the flag is OFF (default) it keeps sending the legacy `visibility` field so production is unaffected until the rollout is greenlit. Backend: register the flag in the frontend-public list so it flows to the web config bootstrap. Tests: legacy toggle still submits `visibility`; flag-on submits `permission_mode`/`invocation_targets`, collapses empty public_to to private, and preserves ticked member grants. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8f51b4f62f |
fix(editor): repair empty list items parsed from a markdown draft (#4869)
The real ordered-list caret bug (MUL-3973). Typing 1. in the comment box persists the draft "1. \n\n"; on remount @tiptap/markdown parses that empty item into a schema-invalid, childless listItem, leaving the document with an AllSelection instead of a text cursor — so the browser paints the caret on the following block and it can't be moved back into the list. Verified against the REAL editor (real createEditorExtensions + @tiptap/markdown, no @tiptap/react mock). Non-empty items round-trip fine; only the empty-item round-trip corrupts, which the reverted #4813 never exercised. - repairEmptyListItems(): rebuild from JSON so every list/task item leads with a paragraph (covers empty items and nested items whose first child is a sub-list); reset the AllSelection first (else setContent collapses the list); keep the whole repair off the undo stack; restore the prior caret (sync path) or land in the list item (mount). - Called in onCreate (mount) and after the sync-effect setContent. - Real-editor tests incl. undo-does-not-revive and nested-list schema validity. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cb68669c73 |
feat(composio): gate MCP apps behind feature flag (#4876)
* feat(composio): server-side connect flow + connections REST (Notion MVP) (MUL-3720) (#4608)
* feat(composio): server-side connect flow + connections REST (Notion MVP) (MUL-3720)
Compose the merged server/pkg/composio SDK into a user-facing connection
manager: signed-state connect handshake, local user_composio_connection
mirror, idempotent disconnect, and a per-user MCP session helper (not yet
wired into task dispatch).
- migration 127_user_composio_connection (no FK/cascade, per DB rules)
- sqlc queries: upsert (idempotent on user_id+connected_account_id), list
active, owner-scoped get, mark revoked
- internal/integrations/composio: signed HMAC-SHA256 state, BeginConnect,
CompleteCallback (idempotent upsert), ListConnections, Disconnect
(upstream 404 = idempotent success), CreateMCPSession (no-op when empty,
pins connected_accounts per toolkit), CallbackRedirect
- REST handlers under /api/integrations/composio (user-scoped, 503 when
COMPOSIO_API_KEY unset): connect/init, callback (302), connections list,
delete
- router wiring gated by COMPOSIO_API_KEY; COMPOSIO_AUTH_CONFIGS_JSON maps
toolkit->auth_config (MVP: notion); state secret from COMPOSIO_STATE_SECRET
or derived from JWT_SECRET; callback base from COMPOSIO_CALLBACK_BASE_URL
or MULTICA_PUBLIC_URL
- tests: state (expire/tamper/wrong-secret), service (mapping, callback
idempotency, non-success, disconnect owner/404 idempotency, MCP pin),
handlers (httptest), redact regression for Bearer mcp_ tokens
MVP scope: Notion only; no task-dispatch overlay, sharing, or webhook
event handling (later stages).
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): bind callback account to user + idempotent revoked disconnect (MUL-3720)
Address PR 4608 review (CHANGES_REQUESTED):
- callback: verify connected_account_id with Composio before mirroring it.
The signed state only proved user/toolkit/exp, so a valid state paired with
a tampered connected_account_id would be written verbatim. CompleteCallback
now calls ListConnectedAccounts and fails closed (ErrAccountVerification)
unless the account belongs to the state's user (composio_user_id == multica
user id) and was created under the toolkit's auth config. No row is written
on mismatch / unknown account / upstream error.
- disconnect: short-circuit to a no-op when the local row is already revoked,
before touching upstream. Previously a second DELETE re-hit Composio and a
non-404 upstream error surfaced as a 502, breaking the 204-idempotent
contract.
- CreateMCPSession: document the v1 single-active-connection-per-(user,toolkit)
constraint and make duplicate selection deterministic (newest-wins, rows are
connected_at DESC) instead of order-dependent map overwrite. Stage 3 owns the
real single-account-enforcement vs multi-account-shape decision.
Tests: tampered/wrong-auth-config/unknown-account callback rejection, revoked-row
disconnect no-op (asserts upstream not re-hit). composio pkg 85% coverage; all
green.
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): list all toolkits + dynamic auth-config resolution (MUL-3720)
Yushen's follow-up to the Notion MVP: surface the full Composio toolkit
catalog, render it in Settings, and drop the static env mapping in favor of
dynamic auth-config discovery.
Config correctness (per Composio docs):
- Remove COMPOSIO_AUTH_CONFIGS_JSON entirely. The toolkit→auth_config mapping
is now resolved at request time from the project's /auth_configs (cached,
5-min TTL), so enabling a toolkit is a dashboard action, not a redeploy.
- Do NOT add COMPOSIO_PROJECT_ID. The project API key (x-api-key) authenticates
to exactly one project; the project is resolved from the key. Only org-level
endpoints use x-org-api-key, which this integration never calls.
Backend:
- SDK: server/pkg/composio/auth_configs.go — ListAuthConfigs (toolkit_slug,
is_composio_managed, show_disabled, limit, cursor).
- service: dynamic resolver (authConfigMap cache; betterAuthConfig prefers a
custom/white-label config over Composio-managed, newest wins); BeginConnect
and CompleteCallback resolve via it; ListToolkits fetches the full catalog
(paginated, capped) annotated with connectable = has an enabled auth config,
connectable-first ordering.
- handler + route: GET /api/integrations/composio/toolkits (user-scoped, 503
when COMPOSIO_API_KEY unset) returning slug/name/logo/category/connectable.
Frontend:
- core: ComposioToolkit/ComposioConnection types, api client methods, and
composio query options (@multica/core/composio).
- views: Settings → Integrations now has a Composio section rendering every
toolkit as a card with search. Connect is gated on `connectable`;
non-connectable toolkits show a muted "not configured" hint instead of a
dead button. Connected toolkits show a badge + Disconnect (with confirm).
- i18n: composio block added to en/zh-Hans/ja/ko settings.
Tests: SDK + service (dynamic resolution, custom-over-managed preference,
connectable flag, resolver-error soft-degrade) and handler toolkits endpoint;
composio pkg 85.7% coverage. go build/vet/gofmt clean; core+views typecheck,
core+views lint, and core tests (691) all green.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): close cross-toolkit callback fail-open by signing auth_config_id into state (MUL-3720)
Re-review blocker: CompleteCallback resolved the toolkit's auth config at
callback time and ignored a resolve error/empty result, while
verifyAccountOwnership skipped the auth-config comparison when the expected
value was empty. A user could then pass another toolkit's connected_account_id
into this toolkit's callback — the owner check passed and it was written under
the wrong toolkit_slug/account binding.
Fix: the auth_config_id is already resolved in BeginConnect (before the state
is signed), so sign it into the state and compare it exactly at callback. No
re-resolve, no fail-open. verifyAccountOwnership now fails closed when the
expected auth config is empty (rejects instead of skipping) and requires an
exact match — closing the cross-toolkit binding gap.
Tests: state round-trips auth_config_id; BeginConnect signs it; callback
rejects wrong/cross-toolkit auth config and an empty (no-mapping) auth config
fails closed. composio pkg 85.2% coverage, all green.
Frontend (non-blocking): the Composio settings tab now surfaces an error when
the connections query fails instead of silently rendering everything as
unconnected.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): hide Settings section entirely when integration unconfigured (MUL-3720)
Decision (option 2, hide-then-merge): don't show a card that leaks the internal
COMPOSIO_API_KEY env-var name to every end user. IntegrationsTab now gates the
whole Composio section (heading + body) on the toolkits query — a 503 means the
key is unset, so the section is withheld instead of rendering the not-configured
card. Admin-only setup guidance is a later, role-gated affordance.
Removed the notConfigured card (and now-unused ApiError import) from
ComposioTab; it only mounts when configured. views typecheck + lint clean.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): Stage 2 frontend polish — callback toast, last_used & expired UI, e2e (MUL-3718) (#4688)
* feat(composio): callback toast + refresh, last_used & expired UI, e2e (MUL-3718)
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): real callback redirect route + StrictMode-safe toast dedup (MUL-3718 review)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): callback endpoint should not require Multica auth (MUL-3843) (#4709)
* fix(composio): move OAuth callback out of the Auth group (MUL-3843)
Composio 302-redirects the browser to /api/integrations/composio/callback
at the end of the OAuth flow, but PR #4608 mounted it inside the cookie-auth
middleware group. When the session cookie is absent (expired session,
SameSite=Strict / Safari ITP, private window, self-hosted callback subdomain)
the Auth middleware returned a hard 401 and a JSON blob instead of the
settings redirect, breaking the flow.
Identity never came from the cookie anyway: it is carried by the HMAC-signed
state param that CompleteCallback verifies (signature, expiry, replay) and
cross-checked by verifyAccountOwnership; h.Composio == nil still 503s. So the
callback is registered alongside the other public OAuth/webhook routes; the
other four composio endpoints stay session-gated.
Refs MUL-3843, MUL-3715.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): correct stale callback routing comments (MUL-3843)
The package header and ComposioCallback doc comments still described the
callback as sitting under the Auth middleware group. After the route was
moved out (this PR), update both to state it is a public route whose identity
comes from the signed state — addressing review nit from 张大彪.
Refs MUL-3843.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): inject MCP overlay into agent runtime at task dispatch (MUL-3721) (#4704)
Stage 3 of the Composio epic. Wires the per-user Composio MCP session into
every agent task so the agent process sees the initiator's connected tools
without any prompt-time plumbing.
Server side
- Migration 128 adds agent_task_queue.runtime_mcp_overlay JSONB plus a
BEFORE-UPDATE trigger that wipes the column on any transition into a
terminal status (completed / failed / cancelled). A trigger is the single
source of truth — future queries that flip status cannot bypass it.
- composio.Service.BuildTaskOverlay(userID) reuses CreateMCPSession and
emits the Claude-style { mcpServers: { composio: { type: http, url,
headers } } } shape the daemon's existing sidecar generators consume.
Returns (nil, nil) on zero active connections so we never burn a
Composio session for a user with nothing to call.
- TaskService grows a Composio ComposioOverlayBuilder seam, wired in
router.go after composiointeg.NewService succeeds. Five enqueue paths
(issue / mention / quick-create / chat / auto-retry) attach the overlay
after CreateAgentTask returns and before the daemon is notified — so
every claim reads a settled row, with no second daemon hop. Best-effort:
a builder failure logs and proceeds with no overlay.
- resolveInitiatorFromTriggerComment derives the initiator user from the
trigger comment when it was authored by a member. Agent-authored
triggers are not treated as initiators (their connected-apps view is
empty by construction).
Daemon side
- handler/daemon.go claim path merges task.runtime_mcp_overlay onto
agent.mcp_config via mergeMCPOverlay before populating
TaskAgentData.McpConfig. Overlay wins on server-name collisions
because it carries the live user-scoped session URL. Errors fall back
to the agent config unchanged — a bad overlay must not surprise-disable
saved MCP tools. The existing execenv sidecar generators (cursor /
codex / openclaw / opencode / hermes-kiro) need no changes: they keep
consuming the merged result through TaskAgentData.McpConfig.
Tests
- 9 merge cases (mcp_overlay_test): both-nil short-circuit, agent-only
pass-through, overlay-only canonicalization, two-side merge, name
collision (overlay wins), top-level key preservation, malformed agent
fallback, malformed overlay fallback, non-object server rejection.
- 4 dispatch cases (composio): zero-connections returns nil without
CreateSession, happy-path emits the right shape with the right user
id, empty-URL defensive branch, SDK error surfacing.
- 4 TaskService helper cases: nil Composio is a no-op (Queries-safe),
invalid initiator does not call the builder, nil overlay skips the
UPDATE, builder error swallowed without panic.
- Migration 128 verified to roll up + down + up cleanly against the test
database.
Out of scope (deferred): assignment-triggered enqueue paths with no
trigger comment get no overlay attached today (no initiator UUID flows
through enqueueIssueTask in that case). Retry paths recompute the overlay
fresh from the parent's initiator_user_id instead of inheriting the bearer
from the parent row, so a stale token can never resurface on a retry.
Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): per-agent allowlist + originator-scoped MCP overlay (MUL-3869) (#4736)
* feat(composio): per-agent allowlist + originator-scoped MCP overlay (MUL-3869)
Stage 3.1 of the Composio epic (MUL-3721 parent). PR #4704 wired in the
runtime_mcp_overlay column and a per-task dispatch hook; this change
inverts the default from "all-on" to opt-in and locks the overlay to the
agent owner's own connected apps:
- Agents carry composio_toolkit_allowlist TEXT[]. NULL or [] => no MCP.
Owner-only read/write; non-owner GET/PUT silently redacts/drops the
field (same shape as mcp_config).
- agent_task_queue carries originator_user_id UUID. Set from the
top-of-chain HUMAN at every enqueue path:
* issue/mention comment by member -> author_id
* issue/mention comment by agent -> inherit via comment.source_task_id
-> parent task originator_user_id
* quick-create -> requester_id
* chat -> initiator_user_id
* retry -> SQL-inherited from parent row
* autopilot -> NULL (system-driven)
- BuildTaskOverlay (composio dispatch) now takes (ctx, originatorUserID,
agent) and short-circuits on five gates: invalid originator,
originator != agent.owner_id, empty allowlist, empty intersection of
allowlist ∩ active connections, defensive empty session URL. Composio
CreateSession is called with BOTH `toolkits.slugs` (the intersection)
AND `connected_accounts` (the pinned account ids), narrowing the
tool-router twice.
- The originator-vs-owner gate closes the agent-fanout privacy hole: any
workspace member who can @-mention a public agent used to project the
owner's connected apps into their run. Now the overlay only mounts
when the human at the top of the chain IS the agent owner.
Tests:
- dispatch_test.go covers all 5 gates plus uppercase/whitespace slug
normalisation.
- task_runtime_mcp_overlay_test.go covers the no-op gates of the new
applyRuntimeMCPOverlay signature.
- agent_composio_allowlist_test.go (handler): owner roundtrip
(list/empty/null), workspace-admin silent-drop, owner-only GET
visibility, pure normaliseComposioToolkitAllowlist.
- resolve_originator_test.go (service, DB-backed): member-authored,
agent-authored inherits via comment.source_task_id, invalid id.
Migration 129 up/down/up verified against docker postgres.
Co-authored-by: multica-agent <github@multica.ai>
* chore(composio): gofmt + regenerate sqlc with v1.31.1 (MUL-3869 review nits)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): accept nested connected account auth config
* feat(views): creator-only MCP tab for per-agent Composio allowlist (MUL-3870) (#4743)
Stage 3.2 frontend on top of the Stage 3.1 backend (MUL-3869,
|
||
|
|
30e1e546cf |
MUL-3355: feat(helm): support external PostgreSQL via postgres.external.enabled
Closes #4237 Add a `postgres.external.enabled` toggle (default: false) so operators can skip the built-in Deployment/PVC/Service and point Multica at any externally managed PostgreSQL (CNPG, RDS, Cloud SQL, Neon, etc.). When enabled: - postgres Deployment, PVC, and ClusterIP Service are not rendered - The hard-coded DATABASE_URL env var is omitted from the backend pod; DATABASE_URL must be supplied in existingSecret and reaches the container through the existing envFrom.secretRef mechanism - POSTGRES_DB / POSTGRES_USER are omitted from the ConfigMap (unused when DATABASE_URL is provided directly) Also update the existingSecret creation example in values.yaml to show both the built-in and external-postgres variants so users know to add DATABASE_URL instead of POSTGRES_PASSWORD when switching modes. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
d4f57aff7a |
MUL-3944: Fix daemon agent discovery around hook wrappers (#4817)
* Fix agent path discovery around hook wrappers Co-authored-by: multica-agent <github@multica.ai> * Fix login shell hook wrapper discovery Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8797667fac |
fix(slack): prefer attachment body over fallback (#4866)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e3e3e7e23f |
fix(realtime): bounded replay window for ShardedStreamRelay on restart (#4875)
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 |
||
|
|
1e85eb0aac |
Fix Kiro ACP usage accounting (#4867)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
942255d283 |
fix(autopilot): keep create_issue runs visible when runtime offline (#4848)
Co-authored-by: JC的AI分身 <tangyuanjc@JCdeAIfenshendeMac-mini.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4ed8f7478f |
fix(server): key reviewer-loop dedup on reviewed commit SHA (MUL-4003) (#4873)
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> |
||
|
|
c7e10ffa85 |
fix(agent): offer no Claude effort levels when the CLI predates --effort
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> |
||
|
|
65269ef922 |
fix(daemon): copy Codex model catalog into task home
Fixes #4825 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a06fc27340 |
fix(web): redirect legacy squads and usage routes (#4833)
Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
84a5853363 |
fix(desktop): restore correct filename in save dialog for attachment downloads (#4296)
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
|
||
|
|
6b70146570 |
test(rollup): serialise shared-singleton rollup tests across packages (MUL-3980) (#4854)
`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>
|
||
|
|
4416313f8f |
docs(changelog): add v0.3.35 entry for the 2026-07-02 release (MUL-3984) (#4856)
* 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>v0.3.35 |
||
|
|
e4994cd431 |
fix(github): allow one installation to bind multiple workspaces (#4855)
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> |
||
|
|
2cf5297814 |
Revert "fix(editor): keep the comment-box caret in the ordered list after switching issues (MUL-3973) (#4813)" (#4851)
This reverts commit
|
||
|
|
03828015ca |
fix(feedback): validate response and pass error kind (#4633)
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 |
||
|
|
c2903365a9 |
fix(editor): keep the comment-box caret in the ordered list after switching issues (MUL-3973) (#4813)
* 📝 [需求] 有序列表光标切换缺陷提案 / 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> |
||
|
|
2e8578acbd |
fix(chat): stop remounting the live timeline on every streamed task message (MUL-3960) (#4845)
* 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> |
||
|
|
c4b116ec3a |
feat(views): expose add-label entry in create-issue dialog (#4846)
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> |
||
|
|
c328fdbcd0 |
fix(issues): wake parent squad leader on same-squad/shared-leader child-done (MUL-3969) (#4843)
* 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> |
||
|
|
0c4c3ff038 |
fix(cli): prevent daemon-managed CLI from silently using user tokens (MUL-3922)
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. |
||
|
|
101cc29e20 |
fix(daemon): time out repo cache git commands
Closes #4795 |
||
|
|
94778449eb |
fix(docker): derive web pnpm version from packageManager (#4815)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
da624a8835 |
feat: add configurable S3 path-style addressing (#4739)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
078f2aa65c |
fix(handler): keep runtime pending redis keys in one slot
Closes #4767 |
||
|
|
ac75a0df72 |
fix(attachments): align text preview whitelist (#4834)
Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
ade6b34e5f |
MUL-3903: Extract shared issue surfaces (#4774)
* 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> |
||
|
|
c3a33fff49 |
fix(markdown): render inline data-URI images (MUL-3961) (#4832)
* 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> |
||
|
|
8b529efc09 |
Fix recovered Antigravity transcript messages (#4814)
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 |
||
|
|
5ed381a9d6 |
Fix comment attachment URL resolution (#4816)
Co-authored-by: multica-agent <github@multica.ai> |