mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 22:59:04 +02:00
v0.3.17
996 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f8bd1d8fc2 |
feat(lark): show real speaker names in Feishu group context (MUL-3084) (#3828)
* feat(lark): resolve real speaker names in group context (MUL-3084) The recent-context block (and quoted/forwarded blocks) labeled senders positionally as "User 1 / User 2", and the agent had no idea who had @-mentioned it. Add APIClient.BatchGetUsers (contact/v3/users/batch) and, on the group prefetch path, resolve the surrounding speakers AND the trigger sender to display names in one batch call. Speakers now render as "[Alice]: ..." and the user's own message as "[Charlie]: ..." so the agent knows who addressed it. Unresolved senders (restricted contact scope, deactivated user) fall back to positional "User N"; resolution is best-effort and never blocks ingestion. Closes the standing speaker-name TODO in the enricher. Co-authored-by: multica-agent <github@multica.ai> * fix(lark): resolve names for quoted/forwarded senders too (review) Address the #3828 review: BatchGetUsers only included the recent-window and trigger senders, so a quoted parent / merge_forward child whose sender was NOT in the recent window still rendered as "User N". Restructure Enrich into fetch (Phase 1) -> resolve names (Phase 2) -> render (Phase 3): quote/forward items are now fetched up front and their senders folded into the single Contact batch, so every block (recent + quoted + forwarded) shows real names in group chats. p2p keeps positional labels. Replaces the fetch+render renderQuoted/renderForwarded with a render-only renderQuotedBlock plus an inline forward fetch. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
05e38e5d37 |
feat(lark): split bind CTA into Feishu and Lark entry points (MUL-3083) (#3832)
* feat(lark): split bind CTA into Feishu and Lark entry points (MUL-3083 follow-up)
The single "Bind to Lark" button began the device flow against
accounts.feishu.cn and relied on a mid-poll tenant_brand="lark" to
auto-switch international users over to accounts.larksuite.com. Lark
users had to scan a QR served from a Feishu domain first, which
surfaced as confusing in real use.
Replace with two explicit CTAs side by side — "Bind to Feishu" and
"Bind to Lark" — and route the device-flow begin straight to the
matching accounts host based on the user's choice. The mid-poll
auto-switch is preserved as a safety net for users who pick the wrong
entry.
Backend
- RegistrationClient.Begin(ctx, namePreset, region): POSTs to
c.cfg.LarkDomain when region=lark, c.cfg.Domain otherwise. Empty /
unknown region falls back to Feishu (matches RegionOrDefault).
- BeginInstallParams.Region threads through to the registration session
and onto runPolling's initial region local. SwitchedDomain still
flips it on tenant_brand=lark.
- POST /api/workspaces/{id}/lark/install/begin accepts ?region=feishu|lark
with empty defaulting to feishu for back-compat.
Frontend
- api.beginLarkInstall(wsId, agentId, region) — region now required
so every call site is forced to pick a cloud explicitly.
- LarkAgentBindButton renders two buttons; dialog state collapsed into
a single dialogRegion useState so an "open but with no region picked"
intermediate state can't exist.
- LarkInstallDialog takes region as a required prop and renders
region-aware copy (title, description, scan hint, link fallback,
success toast).
i18n
- Add bind_button_{feishu,lark}, install_dialog_{title,description}_*,
install_scan_hint_*, install_open_link_fallback_*, and
install_success_toast_* keys across en, zh-Hans, ja, ko. Legacy
single-region keys are kept for now; nothing in the tree references
them anymore but a follow-up cleanup can remove them once the dust
settles.
Tests
- Two new lark.RegistrationClient tests pin region routing in both
directions (region=lark hits LarkDomain; region=feishu hits Domain).
- Two new lark-tab.test.tsx cases pin that clicking each CTA calls
beginLarkInstall with the matching region argument. Existing CTA
tests updated to expect both buttons in place of one.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* fix(lark): bidirectional tenant_brand swap + region-aware badge + link context menu
Addresses Elon's review on PR #3832 plus a separate report that the
"Or tap here to open in Lark" link in the install dialog had no
standard right-click affordances on the desktop app.
Backend (must-fix from review)
The PR's stated 'safety net for users who pick the wrong CTA' only
worked one direction: a Feishu-first begin already swapped to Lark on
tenant_brand=lark, but the new Lark-first begin (added by this same PR)
had no reverse path — a user who picked 'Bind to Lark' but actually
authorized with a Feishu account would carry RegionLark all the way
through finishSuccess and either fail at GetBotInfo or commit a
wrong-region row.
- PollResult now carries SwitchedDomain AND SwitchedRegion in
lockstep, so the caller never has to re-derive region from the
domain string.
- Poll() detects tenant_brand=feishu while polling against a non-Feishu
host symmetrically with the existing tenant_brand=lark check, gated
on the current host so we don't loop on a brand we already match.
- runPolling reads region from res.SwitchedRegion instead of the
hardcoded RegionLark — the SwitchedDomain branch now flips both
feishu→lark and lark→feishu cleanly.
- Tests: updated the existing TestRegistrationClient_Poll_DomainSwitchOnLarkTenant
to assert SwitchedRegion, added TestRegistrationClient_Poll_DomainSwitchOnFeishuTenant
for the reverse, and TestRegistrationClient_Poll_NoSwitchWhenAlreadyOnMatchingHost
(table-driven, both directions) to pin that the gate doesn't loop.
Backend (nit from review)
Handler comment on /lark/install/begin claimed unknown region defaults
to Feishu downstream, but the handler already returns 400 on unknown
values. Updated the comment to match the actual behavior and document
why we 400 rather than silently normalize (so a frontend typo can't
land users on the wrong cloud without telling them).
Frontend (nit from review)
The Agent inspector's Connected badge was hardcoded 'Connected to
Lark' / 'Manage in Lark' (en) and 'Connected to Feishu' / 'Manage in
Feishu' (zh-Hans) — both wrong half the time now that the install
flow can land on either cloud per agent. Made the badge text and
Manage tooltip read from installation.region:
- agent_bot_connected_label_{feishu,lark}
- agent_bot_manage_link_{feishu,lark}
- agent_bot_manage_tooltip_{feishu,lark}
across en / zh-Hans / ja / ko. Legacy single-region keys retained for
safety. Existing badge tests updated: fixtures without 'region' now
expect the Feishu copy; the region: 'lark' test was promoted to also
assert the Lark badge text and link target. 21/21 lark-tab tests pass.
Desktop (separate report)
Right-clicking an <a> in the renderer surfaced only Copy / Cut /
Paste / Select All — no 'Open Link in Browser' or 'Copy Link Address'.
The renderer's <a target="_blank"> click path already routes through
setWindowOpenHandler → openExternalSafely, but discoverability via the
context menu was missing.
context-menu.ts now appends two link-specific items when params.linkURL
is an http(s) URL. Open Link routes through openExternalSafely (reuses
the existing scheme allowlist); Copy Link Address writes to Electron's
clipboard. Labels are localized to the OS preferred language for the
four locales the renderer ships (en / zh-Hans / ja / ko); zh-* variants
all route to zh-Hans, anything else falls back to English. New
context-menu.test.ts pins five cases: link items show for http(s),
not for javascript:/mailto:/etc., not when no link is under the cursor,
zh-CN gets Chinese, fr-FR falls back to English. 198/198 desktop tests
pass.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Jiang Bohan <bhjiang@outlook.com>
|
||
|
|
14f89bc08a |
Fix Claude control request handling (#3827)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
270d177475 |
fix: broken "Add a computer" command on Multica Cloud + two CLI amplifiers (MUL-3087) (#3817)
* fix(server): recognize official cloud by frontend host in daemon setup config The 'Add a computer' dialog builds its command from /api/config's daemon_server_url/daemon_app_url, falling back to 'multica setup' when both are empty. The official cloud is meant to omit them, but the omission only fired when MULTICA_PUBLIC_URL=https://api.multica.ai. When that env is unset the server URL defaults to the frontend origin and the old guard (which required serverURL host == api.multica.ai) didn't match, so the dialog emitted 'multica setup self-host --server-url https://multica.ai' — pointing the daemon backend at the frontend (no /health, no WebSocket proxy). Identify the official cloud by its frontend host alone (multica.ai / app.multica.ai) so a missing or misconfigured MULTICA_PUBLIC_URL can no longer leak the broken self-host command. Regression from #3474. * fix(cli): probe before persisting self-host config to preserve auth on failure setup self-host wrote a fresh CLIConfig{ServerURL, AppURL} (a full overwrite that drops the saved token) and only then probed the server, returning early on failure. A failed probe therefore logged the user out and left them unconnected, with no recovery in the same command. Probe first via persistSelfHostConfigIfReachable: an unreachable server leaves the existing config — and its token — untouched (failed setup = no-op). The prober is injected so both branches are unit-tested. * fix(daemon): serve health before preflight so daemon start readiness is accurate The CLI's 'daemon start' polls the health endpoint for 15s expecting status=running, but the daemon only began serving health after preflightAuth, whose initial workspace sync detects every configured agent's version by exec'ing it (~20s cold with 8 agents). Health served too late, so a perfectly healthy daemon printed 'may not have started successfully'. Start the health server right after resolveAuth (which still fails fast on a missing token) and before the slow preflight, so readiness reflects the daemon core being up rather than agent-version detection finishing. * fix(daemon): gate /health readiness so daemon start can't report a false start Serving health before preflightAuth fixed the false-negative (a healthy daemon printed "may not have started"), but health still returned status:"running" unconditionally — before preflight (PAT renew + workspace sync + runtime registration) had completed. `daemon start` and the desktop treat "running" as ready, so a slow or *failing* preflight could be misreported as a started daemon: setup prints "connected", then the process exits or hangs in agent-version detection with no runtime registered. That is harder to diagnose than the original false-negative. Split liveness from readiness: bind/serve the health port early (so callers see a live "starting" daemon instead of connection-refused), but report status:"starting" until d.ready is set after preflight, then "running". - daemon.go: add d.ready (atomic.Bool); set it true after the background loops launch, before pollLoop. - health.go: healthHandler reports "starting" until ready, else "running". - cmd_daemon.go: `daemon start` waits for "running" with a deadline raised to 45s (covers cold-start agent detection) and a clearer "still starting" message; new daemonAlive() helper treats both "running" and "starting" as a live daemon, so the already-running guard, restart, and stop act on a starting daemon and don't double-spawn or race its listener; `daemon status` shows "starting" distinctly. Older CLIs/desktop that only know "running" safely treat "starting" as not-ready (status != "running"), so no boundary break. Tests: health reports starting-then-running; daemonAlive truth table. Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): handle daemon "starting" health status in lifecycle The daemon now reports /health status:"starting" until preflight completes (liveness/readiness split). That made "starting" a new external contract of /health, but the Desktop daemon-manager only knew "running", so the readiness fix would have moved the CLI's false-negative into a Desktop start regression: - `daemon start` now blocks up to 45s waiting for readiness, but the Desktop spawned it via execFile({ timeout: 20_000 }). On a cold start (the ~20s agent detection this PR targets) Electron killed the CLI supervisor at 20s and reported a start failure, even though the detached daemon child kept booting — the UI flashed "stopped" then "running". Raise the timeout to 60s (must exceed the CLI's 45s startupTimeout). - The Desktop treated only raw status === "running" as a live daemon, so a daemon that was still "starting" (booting on its own or started via the CLI) showed as "stopped", and startDaemon() would spawn a second one — which the new CLI rejects as "already running", surfacing as a start error. Add daemonStatusAlive() (shared, pure, unit-tested) mirroring the Go daemonAlive() and use it for liveness: fetchHealth() surfaces a daemon-reported "starting" as state "starting" regardless of our own currentState; startDaemon()'s already-running guard and the restart-on-user-switch guard treat "starting" as an existing daemon. version-decision stays gated on "running" (readiness, not liveness) — unchanged. Verified: desktop typecheck, eslint, full vitest suite (193 tests) all pass. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6d0b9e3918 |
feat(lark): prefetch surrounding group context on @-mention (MUL-3084) (#3819)
* feat(lark): prefetch surrounding group context on @-mention (MUL-3084) In Feishu group chats the Bot only saw the single message that @-mentioned it — never the surrounding conversation — because the inbound enricher only inlined context the user explicitly attached (a quoted reply or a merge_forward), and the API client had no way to list a chat's history. Add APIClient.ListChatMessages (GET /open-apis/im/v1/messages, container_id_type=chat, ByCreateTimeDesc, page_size clamped to Lark's 50 cap) and, for a group message addressed to the Bot, prefetch a bounded window of recent messages and inline them as a <recent_context> block ahead of the user's own message. The trigger and any quoted parent are excluded so nothing is duplicated; speakers are labeled positionally (User 1/2 / Bot); failures degrade to a visible placeholder and never block ingestion. Window size is configurable via InboundEnricherConfig.RecentContextSize (<=0 disables); production wires DefaultRecentContextSize (20). One list call per addressed turn keeps the fetch within the inbound ACK / EnrichTimeout budget. Co-authored-by: multica-agent <github@multica.ai> * feat(lark): anchor group context window to trigger time, default 10 Address review feedback on MUL-3084: - Anchor the recent-context prefetch to the trigger message's time: thread the message create_time through InboundMessage and pass it as the list end_time (millis -> seconds), so the window is the conversation up to the @-mention rather than whatever is newest when the slightly-later prefetch HTTP call runs. end_time is omitted when the time is missing/unparseable (falls back to newest N). - Lower DefaultRecentContextSize from 20 to 10. Co-authored-by: multica-agent <github@multica.ai> * docs(lark): clarify recent-context persistence stance and fetch-window semantics Co-authored-by: multica-agent <github@multica.ai> * fix(lark): region-aware doJSON for ListChatMessages after rebase origin/main merged #3815 (Lark dual-region support), which changed doJSON to take a per-call baseURL resolved via resolveBaseURL(creds). Adapt the new ListChatMessages call to that signature so the backend build passes against latest main, and refresh the now-stale ListMessagesParams comment (EndTime is exposed). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6ac8314711 |
feat(lark): support both Feishu and Lark from one deployment (MUL-3083) (#3815)
* feat(lark): serve Feishu and Lark from one deployment, per installation
The Lark integration was locked to a single open-platform host chosen
deployment-wide (MULTICA_LARK_HTTP_BASE_URL / _CALLBACK_BASE_URL,
defaulting to open.feishu.cn), so one deployment could talk to only the
mainland Feishu cloud OR Lark international — never both. Teams on the
other tenant could not use the integration at all.
Make the host per-installation. The device-flow installer already
auto-detects the tenant (Lark emits tenant_brand="lark" mid-poll); we now
persist that as lark_installation.region, carry it on
InstallationCredentials.Region, and resolve the open-platform host per
call (REST + WS bootstrap) from the region. An explicit cfg.BaseURL
(env / httptest) still overrides every region, so existing tests and
staging/proxy setups keep working.
- migration 116: lark_installation.region TEXT NOT NULL DEFAULT 'feishu'
CHECK (region IN ('feishu','lark')) — existing rows are all mainland.
- lark.Region enum + OpenPlatformBaseURL/RegionOrDefault helpers.
- registration: thread the detected region into finishSuccess so the
install-time GetBotInfo hits the right cloud AND the row records it.
- every credential-build site (patcher, replier, WS provider, union_id
backfill) copies region off the installation row.
- region is part of the WS supervisor fingerprint so a re-install that
switches cloud restarts the connection.
- API: surface region on the installation listing DTO.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* feat(lark): surface installation region in settings UI
Read the per-installation region off the listings response: build the
"Manage in Lark" dev-console host from it (open.feishu.cn vs
open.larksuite.com instead of a hardcoded mainland host) and render a
Feishu / Lark badge on each connected bot. The field is optional and
defaults to Feishu when an older server omits it (API-compat). Adds the
region_feishu / region_lark labels to all four locales.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* docs(lark): document simultaneous Feishu + Lark support
The cloud each bot belongs to is now auto-detected at install and stored
per installation, so one deployment serves both. Replace the old
"point MULTICA_LARK_HTTP_BASE_URL at larksuite for international tenants"
guidance (now just an optional override) in all four locales.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* fix(lark): repair legacy Lark-international installs on upgrade
Review follow-up (MUL-3083). Migration 116 backfilled every existing
lark_installation to region='feishu', assuming all historical rows were
mainland. But self-host deployments could already run Lark international
via the deployment-wide MULTICA_LARK_HTTP_BASE_URL override, so those
rows are really Lark — clearing the override after upgrade (which the new
docs invite) would route them to open.feishu.cn and break them.
Add a one-shot startup repair, BackfillRegionFromLegacyOverride, fired
off the hot path like BackfillBotUnionIDs: when the deployment's global
base-URL override targets open.larksuite.com, relabel the still-default
'feishu' rows to 'lark'. Gating on the deployment-wide override is what
makes it safe — every pre-existing install on such a deployment was Lark.
Idempotent; no-op on mainland / fresh deployments. Verified end-to-end
against a scratch DB (flip then 0-row idempotent re-run).
Also document that a Lark/飞书 app_id is globally unique across both
clouds, which is what makes the app_id-keyed token cache and the
UNIQUE(app_id) constraint safe across regions (review nit).
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* docs(lark): fix ops guidance to match auto per-installation region
Review follow-up (MUL-3083). .env.example and docker-compose.selfhost.yml
still told operators that international Lark requires pointing both base
URLs at open.larksuite.com — now wrong, and it would push a fresh
deployment back into a single-cloud override. Rewrite them: the base
URLs are optional deployment-wide overrides; normal dual-cloud operation
keeps them empty. Document the first-boot auto-relabel for deployments
migrating off the old single-cloud override, across the integration docs
(en/zh/ja/ko).
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
905ebbdde1 |
fix(github): populate connected account name on install [MUL-3078] (#3811)
* fix(github): populate connected account name on install [MUL-3078]
The Settings → GitHub connection card was rendering 'Connected to
unknown' because:
1. fetchInstallationAccount in the setup callback hit GitHub's
/app/installations/{id} endpoint unauthenticated. That endpoint
requires App JWT auth; the call returned 401, and the function
fell through to the 'unknown' placeholder which was persisted as
account_login.
2. The installation webhook handler did upsert the row with the real
login when GitHub later delivered installation.created, but it
never published a github_installation:created event. The frontend
query stayed stale, so the UI kept showing 'unknown' even after
the row had been refreshed.
Fix:
- Add optional GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY env vars. When
set, signGitHubAppJWT mints a short-lived RS256 JWT (back-dated 60s
for clock skew, capped at 9m to stay inside GitHub's 10m max) and
fetchInstallationAccount uses it as a Bearer token. The setup
callback now writes the real org/user name on install.
- When the new env vars are not configured, the call still falls
through to 'unknown' as before — but the webhook handler now
publishes EventGitHubInstallationCreated after the upsert, so the
realtime listener invalidates the installations query and the UI
converges to the real value within seconds, no manual refresh.
Tests cover JWT signing (claims, signature, malformed PEM, partial
config), fetchInstallationAccount with a JWT-gated httptest mock,
and the webhook refresh + broadcast on a seeded 'unknown' row.
Docs updated for .env.example and github-integration /
environment-variables in en, zh, ja, ko.
Co-authored-by: multica-agent <github@multica.ai>
* test(github): defuse JWT clock-bomb by injecting parser time [MUL-3078]
PR review caught that TestSignGitHubAppJWT_ClaimsAndSignature signed the
token with a fixed 'now' (2026-06-05T12:00:00Z) but parsed it with a
default jwt.Parse, which uses real time.Now() for exp validation. Once
real wall clock crossed the token's exp (now + 9m = 12:09:00Z), the
test would have flipped to a deterministic failure on every CI run.
Inject the same fixed 'now' into the parser via jwt.WithTimeFunc so
both signing and validation share one clock. Verified independently
that without the fix the parser rejects the token as 'expired', and
with the fix it accepts.
Also clarified the fetchInstallationAccount comment to be unambiguous
about what 'do not block' actually means: the HTTP call IS synchronous
(no independent timeout, pre-existing wart), but a failure here just
falls back to the unknown placeholder rather than aborting the
callback.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
3708fb0f07 |
fix(daemon): inactivity-based agent run timeout, no wall-clock guillotine (MUL-3064)
Active long-running sessions are no longer killed by a fixed wall-clock deadline. Liveness is delegated to the idle watchdog (MULTICA_AGENT_IDLE_WATCHDOG, default 30m) with a larger in-flight-tool budget (MULTICA_AGENT_TOOL_WATCHDOG, default 2h). MULTICA_AGENT_TIMEOUT is an opt-in absolute cap (default 0 = no cap). The server-side 2.5h sweeper is unchanged as a coarse backstop. Fixes #3745. |
||
|
|
76dbb87762 |
fix(agent): standardize model-discovery timeouts to 15s, stop caching empty results
Raise pi and cursor model-list discovery timeouts 5s->15s to match opencode/ACP; openclaw stays 30s (sequential multi-spawn). Stop caching empty discovery results so a transient timeout doesn't keep the picker blank for the full TTL. Fixes #3729. MUL-2977. |
||
|
|
62925b97f1 |
chore(cli): remove the --from-template flag from agent create (#3805)
* chore(cli): remove the --from-template flag from agent create
The `--from-template` CLI flag was an untaught, immature surface (the
built-in skill's source-map explicitly marked the template path "out of
scope"). It also silently ignored sibling create flags (--custom-env,
--mcp-config, etc.) by short-circuiting before body assembly. Remove the
flag and its runAgentCreateFromTemplate handler from the CLI.
Scope is CLI-only. The agent-template product feature stays intact:
- registry server/internal/agenttmpl/ (embedded curated templates)
- handler server/internal/handler/agent_template.go
- routes GET /api/agent-templates, GET /api/agent-templates/{slug},
POST /api/agents/from-template
- the onboarding "create from template" flow (packages/views/onboarding)
The onboarding flow calls the API directly and does not depend on the
CLI flag, so removing the flag does not affect it.
Updates the multica-creating-agents source map accordingly.
MUL-3070
Co-authored-by: multica-agent <github@multica.ai>
* fix: correct source-map note on agent-template usage + guard --from-template
Review of #3805 (MUL-3070) flagged a factual error in the source-map note:
it claimed onboarding uses the agent-template backend. It does not.
`packages/views/onboarding/steps/step-agent.tsx` builds four hardcoded
local presets (i18n-resolved) and creates via plain `POST /api/agents`
(`createAgent`), never `POST /api/agents/from-template`. The whole
agent-template stack (registry, handler, routes, `packages/core` client +
query wrappers) is orphaned — the removed CLI flag was its only non-test
caller. Rewrite the note to say so.
Also add a regression test asserting `agent create` exposes no
`--from-template` flag, so it can't be silently re-added.
MUL-3070
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
3caba86b09 | feat(scheduler): DB-backed execution-record scheduler [MUL-2957] | ||
|
|
63b847ee48 |
Honor agent identity in assignment workflow (#3802)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
18a5224fe8 |
feat(cli): add --mcp-config flags to agent create/update (#3799)
Agents already support an mcp_config field (consumed by the daemon → provider at task time) and the agent-settings UI exposes an MCP tab, but the CLI had no way to set it. This adds the missing CLI surface, mirroring the existing custom-env pattern: - `agent create` and `agent update` gain --mcp-config / --mcp-config-stdin / --mcp-config-file. The stdin/file channels keep MCP server tokens out of shell history and 'ps'; the three channels are mutually exclusive. - The value is validated as a JSON object (or the literal `null` to clear, on update), matching the agent-settings MCP tab. Empty stdin/file input errors instead of silently clearing a secret-bearing field. - Unlike custom_env, mcp_config IS settable via `agent update` — it is persisted through the generic UpdateAgent endpoint (no dedicated audited endpoint), so both create and update expose the flags. Adds parser/resolver unit tests (incl. secret-leak sanitization) and updates the multica-creating-agents built-in skill + source map. MUL-3070 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c9ceaee4d9 |
fix(agent): stop stripping user-facing CLAUDE_CODE_* config from child env (#3690)
* fix(agent): stop stripping user-facing CLAUDE_CODE_* config from child env isFilteredChildEnvKey blanket-removed every CLAUDE_CODE_* var from the spawned Claude Code child's environment. The intent was only to keep the daemon's internal session markers from leaking, but CLAUDE_CODE_* is also Anthropic's user-facing config namespace. On Windows this stripped the user-set CLAUDE_CODE_GIT_BASH_PATH, so Claude Code could not locate bash.exe, exited immediately, and every task failed with "write claude input: write |1: The pipe has been ended." Switch from prefixing the whole CLAUDE_CODE_ namespace to an exact-name denylist of the internal runtime/session markers (CLAUDECODE, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_EXECPATH, CLAUDE_CODE_SESSION_ID, CLAUDE_CODE_TMPDIR, CLAUDE_CODE_SSE_PORT), still blanket-stripping the wholly-internal CLAUDECODE_* namespace. Every other CLAUDE_CODE_* var (GIT_BASH_PATH, USE_BEDROCK, USE_VERTEX, MAX_OUTPUT_TOKENS, ...) now reaches the child. The internal-marker set was confirmed against the live runtime, not guessed. Fixes the whole class, not just git-bash: Bedrock/Vertex/etc. were silently dropped the same way. MUL-2940 Co-authored-by: multica-agent <github@multica.ai> * fix(agent): keep CLAUDE_CODE_TMPDIR in child env CLAUDE_CODE_TMPDIR is a documented, user-configurable temp-dir override (public env-vars reference), not an internal per-session marker. Claude Code creates its own per-session subdir under it, so inheriting it is harmless — and stripping it would silently break a user's temp-dir override the same way the broad prefix filter broke CLAUDE_CODE_GIT_BASH_PATH. Drop it from the internal denylist (which now holds only the undocumented per-process runtime markers: CLAUDECODE, CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_EXECPATH, CLAUDE_CODE_SESSION_ID, CLAUDE_CODE_SSE_PORT) and assert it reaches the child. MUL-2940 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f3ab29cdfc |
fix(lark): publish lark_installation:created at row-commit, not on status poll (#3770)
The agent Integrations tab's "已连接到飞书" connection badge only updated after a manual page refresh. lark_installation:created had a single emit site — the status-poll handler GetLarkInstallStatus — so it only fired while a browser was actively polling the install dialog to success. Every other surface (a second admin, the inspector sidebar, the Settings panel, or the installer whose dialog closed before the success poll) never received the invalidation frame, and under the QueryClient defaults (staleTime: Infinity) the installations cache stayed stale until a full page refresh. Publish the event from RegistrationService.finishSuccess at the row-commit point, mirroring the already-correct revoke path, so every workspace client refreshes the moment the install lands. Wire the bus via an optional SetEventBus (keeps the constructor and its validation tests untouched, nil-safe) and remove the now- redundant poll-handler emit. MUL-3059 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5e1a6c4853 |
fix(cli): degrade 'issue metadata list' to {} on /metadata 404 (#3757)
Self-hosted backends without the per-issue metadata route (older builds,
unapplied 105_issue_metadata migration, or proxy/ingress misroutes) reply
404 to GET /api/issues/:id/metadata. The agent runtime bootstrap calls
'multica issue metadata list <issue> --output json' best-effort, but a
non-zero exit was being escalated by Hermes into a failed agent run even
when the rest of the work succeeded.
This makes only the 'list' verb best-effort: a 404 from /metadata now
prints {} (or an empty table) and exits 0. Other status codes (401, 500,
etc.) keep real error semantics, and 'metadata get / set / delete' are
unaffected — those represent explicit caller intent.
To support the status-code check without changing the user-facing error
string, GetJSON now returns *cli.HTTPError on HTTP failures (the format
'GET <path> returned <code>: <body>' is preserved by HTTPError.Error()).
Refs GitHub issue #3711.
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
569b43136c |
fix(editor): download attachments without blank web tab (#3752)
* fix(editor): download attachments without blank web tab Co-authored-by: multica-agent <github@multica.ai> * fix(attachments): preserve workspace in web download URLs Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7fdec9e6e4 |
Teach default PR handoff in issue skill (#3753)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c99c2493ae |
fix(agent): keep resolvable models when CLI discovery exits non-zero
Parse the discovered catalog even when the model-discovery CLI exits non-zero (pi/opencode/cursor/openclaw) instead of discarding it and returning an empty model picker. Filter pi diagnostic lines so stale-pattern warnings don't coin bogus models. Fixes #3729. MUL-2977. |
||
|
|
ae27058b0a |
fix(attachments): unified download endpoint with mode + presign + proxy (MUL-2976) (#3747)
Fix attachment download for self-hosted deployments using private S3-compatible buckets without CloudFront. Closes #3721. **Server** - New unified `GET /api/attachments/{id}/download` endpoint that picks CloudFront / S3 presign / server proxy at request time. - `ATTACHMENT_DOWNLOAD_MODE=auto|cloudfront|presign|proxy` and `ATTACHMENT_DOWNLOAD_URL_TTL` env knobs; `auto` routes Docker hostnames / localhost / private IPs through the proxy and public S3 endpoints through presign. - `Storage.PresignGet` capability; S3 implementation generates presigned GET URLs. - `attachmentToResponse` returns the unified relative endpoint instead of leaking raw unsigned S3 URLs when CloudFront is not configured. Proxy path streams via `io.Copy` with `Content-Disposition` / `Content-Length` / `Cache-Control: no-store` / `X-Content-Type-Options: nosniff`. **Clients** - CLI / Desktop / Mobile resolve relative `download_url` values against the configured API base. Desktop covers the Electron native download bridge and the media preview modal; Mobile covers `Linking.openURL`, the markdown image RN loader, and the composer's completed non-image file chip. - Mobile gains a minimal Node-environment vitest lane wired into `mobile-verify.yml`. **Docs** - `.env.example`, `docker-compose.selfhost.yml`, `SELF_HOSTING_ADVANCED.md`, and the `environment-variables` doc set updated with the new env keys and the `ATTACHMENT_DOWNLOAD_MODE=proxy` recommendation for Docker / VPC-internal object stores. **Tests** - `internal/storage`, `internal/cli`, `internal/handler` (download endpoint, mode selection, proxy header, `/content` non-regression), `cmd/server` (trusted proxy parser). - `packages/views/editor/use-download-attachment.test.tsx` and `attachment-preview-modal.test.tsx` exercise relative URL resolution + absolute pass-through. - `apps/mobile/lib/attachment-url.test.ts` covers every helper branch plus the composer non-image chip case. |
||
|
|
8db619c1cd |
fix(email): wire SMTP_EHLO_NAME through self-host config + docs [MUL-2984] (#3749)
* fix(email): wire SMTP_EHLO_NAME through self-host config + docs Follow-up to #3679, which added SMTP_EHLO_NAME in code but never exposed it to operators. - docker-compose.selfhost.yml: pass SMTP_EHLO_NAME through to the backend container. The compose env block is an explicit allowlist, so without this the override set in .env was silently dropped and never reached the process — making the escape hatch unusable on the docker path. - Document the var alongside its SMTP_* siblings: .env.example, SELF_HOSTING_ADVANCED.md, environment-variables.mdx, auth-setup.mdx, and self-host-quickstart.mdx (the last two with a strict-relay example). - email.go: log when os.Hostname() fails instead of silently falling back to net/smtp's lazy "localhost" — the exact greeting strict relays reject. - Add TestNewEmailService_EHLOName covering the env override, trimming, and the hostname fallback. MUL-2984 Co-authored-by: multica-agent <github@multica.ai> * fix(email): gate EHLO resolution to SMTP mode + sync docs to zh/ja/ko Addresses review nits on this PR: - email.go: resolve smtpEHLOName only when SMTP_HOST is set, so the Resend / DEV-stdout paths never call os.Hostname() or emit its failure log. The EHLO name is only ever used on the SMTP send path. - docs: add SMTP_EHLO_NAME to the zh/ja/ko variants of environment-variables, self-host-quickstart, and auth-setup, in sync with the English docs updated earlier in this PR. Note: the ja/ko self-host-quickstart and auth-setup pages were already missing the port-465 implicit-TLS example (pre-existing i18n drift from an earlier SMTP_TLS change, unrelated to this PR); the new EHLO block is inserted at the correct logical anchor regardless. A full ja/ko re-sync is left as a separate follow-up. MUL-2984 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a9f0739b52 | fix(email): set EHLO hostname for SMTP relay compatibility (#3679) | ||
|
|
b9334dd59f |
fix: anchor comment triggers to thread roots (#3746)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9beb45b55b |
fix(chat): deliver every debounced user message to the agent (MUL-2968) (#3744)
The Lark short-window debounce (MUL-2968, #3742) can land several user messages in a chat session before a single agent run fires. But the daemon claim built the agent prompt from only the *single most recent* user message (walk history backward, take first user message, break). So 「看上海天气」then「还有青岛」debounced into one run, and the agent received only 「还有青岛」— it answered Qingdao and never saw Shanghai. The session itself was correct (both messages persisted); the gap was in what the run delivered to the agent. Before debouncing this was masked because each message got its own run. Build the prompt from the whole unanswered set instead: the trailing run of user messages after the last assistant reply (every completed/failed run writes an assistant row, so the anchor advances one turn at a time — the full burst on the first turn, only the new message(s) after a reply). Attachments are collected from each included message. Extracted the selection into a pure trailingUserMessages helper with table-driven unit tests, plus a DB-backed claim test asserting both messages reach the agent and that a post-reply message delivers alone. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6e004149a8 |
feat(lark): debounce inbound run trigger per chat session (MUL-2968) (#3742)
A forwarded transcript plus a follow-up note arrive as two separate Lark messages, each of which synchronously called EnqueueChatTask — so the bot ran twice (once on the bare forward, before the note arrived). The chat task already reads the whole session history at run time, so the messages never needed stitching; only the run TRIGGER did. Introduce pendingBatcher: a per-chat_session debouncer that collapses a burst into one agent run on a 3s silence window. Each message is still appended, deduped, and ACKed synchronously and individually; step 8 of the dispatcher now schedules a debounced flush instead of enqueuing inline. Because EnqueueChatTask's agent-offline / agent-archived verdict is now only known at flush, the dispatcher emits that notice itself via an injected FlushReply (wired to OutcomeReplier.Reply) rather than returning it synchronously to the hub. Infra failures are logged, not surfaced — the inbound frame was ACKed long ago. The hub drains the batcher on graceful shutdown so a normal restart does not drop a pending window. Out of scope (owner-aligned): group-chat multi-speaker batching, restart recovery for the in-process window, and forwarded-sender real-name resolution. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5eba94ee25 |
feat(lark): inbound context enrichment — post / merge_forward / quoted-reply (MUL-2951) (#3724)
Expand an inbound Lark bot message's body before dispatch with the context
a user explicitly attached, so the agent sees a semantically complete
conversation instead of a bare "@bot 总结一下".
- post: flatten rich-text (title + paragraphs, links, @-mentions) to plain
text synchronously in the decoder.
- merge_forward: inline the forwarded transcript via a single GetMessage —
GET /open-apis/im/v1/messages/{id} returns the forward sentinel plus the
bundled children. (The issue's container_id_type=merge_forward query is
undocumented; this avoids it and also handles a forwarded quoted parent.)
- quoted reply: prepend the parent_id message as a <quoted_message> block;
a parent that is itself a forward nests a <forwarded_messages> block.
- new InboundEnricher runs in the WS connector between decode and emit,
bounded by EnrichTimeout and degrading to "[unable to fetch]" placeholders
so it never blocks the ~3s long-conn ACK budget.
/issue stays parseable on a quote-reply by parsing the command from the
user's own text (CommandBody) rather than the enriched body.
Short-window debounce batching (issue item #4) is tracked as a follow-up.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
945d684afd |
MUL-2965 fix(metrics): harden business sampler quality (#3738)
* fix(metrics): harden business sampler quality (MUL-2965) Co-authored-by: multica-agent <github@multica.ai> * fix(metrics): alert on sampler acquire failures Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b92c3fbc93 |
chore(analytics): stop shipping operational events to PostHog (MUL-2967) (#3720)
* chore(analytics): stop shipping operational events to PostHog (MUL-2967) Operational / execution-lifecycle telemetry dominated PostHog event volume and drove the bill: runtime_offline alone was ~54% of ~22.6M events/mo, and ~99% of events were billed at the higher identified-event rate. These signals already have Prometheus counters (Grafana), so the PostHog copies were redundant cost. - Add analytics.IsMetricsOnly; metrics.RecordEvent now skips the PostHog Capture for runtime_* and autopilot_run_* while still incrementing their Prometheus counter (their analytics.Event constructors are retained to feed the metric label set via IncForEvent). - Remove the agent_task_* PostHog path entirely: drop captureTaskEvent and the AgentTask* constructors/constants. Their Prometheus side is unchanged via the typed BusinessMetrics.RecordTask* methods. Also remove the now-dead taskDurationMS / willRetryTask helpers. - Update the pairing lint test (no agent_task allow-list, no naked-Capture exception), add a RecordEvent skip test + IsMetricsOnly test, and update docs/analytics.md (taxonomy, per-event banners, reconciliation). Product/funnel events (signup, onboarding, issue_created, issue_executed, chat_message_sent, agent_created, autopilot_created, etc.) are unchanged and still ship to PostHog. Co-authored-by: multica-agent <github@multica.ai> * docs(analytics): correct agent_task Prometheus metric contract (MUL-2967) Address PR review: the agent_task_* "Prometheus-only" banner claimed the old PostHog event properties (task_id, agent_id, duration_ms, error_type, will_retry, ...) were the metric label set. They are not — the real labels are only source/runtime_mode/provider/terminal_status/failure_reason. - Replace the agent_task_* sections with the actual metric names and labels (multica_agent_task_*; see business.go / labels.go), and explain that completed/failed/cancelled are terminal_status values on multica_agent_task_terminal_total, with wall-clock in the *_seconds histograms. - Tighten the runtime_*/autopilot_run_* banners so id properties aren't mistaken for labels. - Drop the stale AgentTask allow-list reference from the pairing lint test header comment. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
598a6c51f2 |
refactor(server/lark): collapse HTTP_ENABLED + WS_ENABLED into the SECRET_KEY gate (MUL-2671) (#3717)
MULTICA_LARK_HTTP_ENABLED and MULTICA_LARK_WS_ENABLED were staging knobs from the multi-PR rollout of the Lark MVP — they let the DB schema + inbound dispatcher land before the HTTP wire was real, and before the WS long-conn protocol was wired. Now that the MVP has shipped end-to-end, "I set SECRET_KEY but I don't want to talk to Lark" is not a useful production state: setting the at-rest master key is the operator's opt-in for the integration as a whole. Collapse the gate down to MULTICA_LARK_SECRET_KEY alone. When the key is present, wire the real HTTPAPIClient + the real WSLongConnConnector. CI / integration tests that want stub-style behaviour can point MULTICA_LARK_HTTP_BASE_URL at a mock server (already supported) instead of toggling a separate flag. Host overrides (HTTP_BASE_URL, REGISTRATION_DOMAIN, CALLBACK_BASE_URL) stay — those are real ops needs for international tenants / staging. stubAPIClient + NoopConnectorFactory remain exported because the test suite uses them directly; only the router boot path stops reaching for them. The connector factory keeps its noop fallback for the case where the endpoint fetcher fails to construct, so a malformed MULTICA_LARK_CALLBACK_BASE_URL degrades gracefully (visible as "connector=noop" in the boot log) instead of panicking the server. Lark integration + handler tests still pass; go vet clean. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d6a556bdbf |
fix(execenv): refresh skills in place on reuse instead of accumulating duplicate dirs (#3716)
Re-dispatching the same agent on the same issue reuses the persistent workdir via execenv.Reuse(), where the standard-provider skill refresh re-wrote skills without clearing the prior dispatch's output, so allocateCollisionFreeSkillDir dodged Multica's own directories into issue-review-multica-N. On reuse, reclaim the platform-owned managed skill directories the prior manifest recorded (removeReusedManagedSkillDirs) and roll back the remaining sidecar files (CleanupSidecars) before refreshing, so each skill lands at its canonical slug every dispatch. Mirrors the Codex hydrateCodexSkills wipe; scoped to reuse, which never runs for local_directory tasks. Fixes #3684 (MUL-2963). |
||
|
|
8c98940b79 |
Lark Bot integration MVP: migration + service boundary (MUL-2671) (#3277)
* feat(db): add Lark integration migration (MUL-2671) Introduces seven tables for the 飞书 Bot integration MVP — per-agent PersonalAgent installations, user/chat bindings, inbound dedup + non-content drop audit, outbound card mapping, and short-lived single-use member binding tokens. Schema notes: - chat_session schema unchanged; Lark routes through a separate binding table rather than adding a metadata JSONB column. - Outbound card mapping is task/message scoped so multiple runs on the same session can't stomp each other's cards. - lark_inbound_audit stores routing / identity / drop_reason ONLY, never message body — the audit channel for unbound users and group messages that don't address the Bot. - app_secret stores ciphertext (encryption helper lands in a follow-up commit on this branch); DB never sees plaintext. Co-authored-by: multica-agent <github@multica.ai> * feat(util): add secretbox AES-256-GCM helper for at-rest secrets First consumer is lark_installation.app_secret (MUL-2671 §4.4), but the helper is intentionally generic — future per-tenant secrets that must not appear in a DB dump can reuse it. Construction: AES-256-GCM with a per-message random nonce, providing authenticated encryption. Tampered ciphertext fails Open instead of silently decrypting to garbage. Master key loaded from a base64 env var via LoadKey; key rotation is not in scope yet. Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): extract IssueService.Create as single create entry (MUL-2671) Establishes the service-layer boundary mandated by Elon's 二审 of MUL-2671 §4.8: issue creation no longer lives inside the HTTP handler. Both the HTTP POST /issues handler and the future Lark /issue command call into service.IssueService.Create, so duplicate guard, issue numbering, attachment linking, broadcast, analytics, and agent/squad enqueue stay aligned. Handler responsibilities shrink to parsing the HTTP request, doing actor resolution / validation (transport-specific), and converting service results into the IssueResponse + 201. The transaction-wrapped core, attachment link, event publish, analytics capture, and agent/squad enqueue all move into service.IssueService.Create. A BroadcastPayload callback on the service keeps the WS broadcast shape (the full IssueResponse) without forcing the service to depend on handler-layer response types. Co-authored-by: multica-agent <github@multica.ai> * feat(integrations): add Lark package skeleton (MUL-2671) Establishes the architectural boundaries Elon's 二审 mandated as first-PR blockers without dragging in OAuth, WebSocket, or card-patching code (those land in follow-up PRs): - ChatSessionService interface — channel-aware chat-session entry point for Lark, deliberately separate from the HTTP SendChatMessage handler. The HTTP handler's single-creator guard (creator_id == request user_id) is correct for the browser client but rejects group chat_sessions by construction; Lark needs its own service. - AuditLogger interface — the only path for recording dropped events. Its signature deliberately omits message body, enforcing the drop-audit policy (MUL-2671 §4.7) at the type level: unbound users and non-addressed group messages can't accidentally end up in chat_session. - Typed IDs (OpenID, ChatID) prevent UUIDs from being conflated with Lark-side identifiers at compile time. - DropReason constants align dashboard/audit queries across callers. Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): move parent/project workspace check into IssueService (MUL-2671) Parent existence and project workspace membership now live inside IssueService.Create, inside the same transaction as the duplicate guard and counter increment. The HTTP handler stops re-implementing the lookup; every future create entry (Lark /issue, MCP, API keys) inherits the same boundary without copy-pasting the SQL. Adds two error sentinels (ErrParentIssueNotFound, ErrProjectNotFound) so transports can translate to their own error shapes. Handler-level cross-workspace tests guard the boundary against future regressions. Co-authored-by: multica-agent <github@multica.ai> * fix(db): harden Lark migration safety底座 — TTL cap + workspace FK (MUL-2671) Two storage-layer hardenings that move the must-fix line off "the app layer enforces it" and onto the schema itself, so future write paths or hand-inserted rows cannot regress the invariants. 1) lark_binding_token TTL cap. The DB CHECK was 1 hour as defense-in-depth while the app constant was 15 minutes; the CHECK now matches the product cap (15 minutes). Application constant docstring updated to reflect that storage enforces the same bound. 2) lark_user_binding workspace membership. The table previously only FK'd to workspace / user / installation independently, so a binding could exist for a user no longer in the workspace, or claim a workspace different from its installation's. Two composite FKs close the gap structurally: * (installation_id, workspace_id) → lark_installation(id, workspace_id) — guarantees a binding's workspace_id always matches its installation's workspace_id. A new UNIQUE (id, workspace_id) on lark_installation is added as the FK target. * (workspace_id, multica_user_id) → member(workspace_id, user_id) with ON DELETE CASCADE — when a user is removed from the workspace, the binding cascades away in the same transaction. There is no longer a path where lark_user_binding outlives workspace membership. These two FKs are the schema-level proof for §4.3's "unbound or non-workspace members cannot leak content into chat_session" invariant. Co-authored-by: multica-agent <github@multica.ai> * feat(integrations/lark): inbound services + /issue dispatcher (MUL-2671) Lands the inbound service layer for the Lark Bot MVP, sitting on top of the migration + service-boundary scaffold from the previous commits. What ships: - sqlc queries for all seven lark_* tables (idempotent dedup insert, CAS WS-lease, single-use binding-token consume, etc.) plus GetMostRecentUserChatMessage for the /issue fallback. - AuditLogger backed by lark_inbound_audit; signature deliberately body-free so callers cannot leak content into the drop log. - ChatSessionService: find-or-create chat_session via the binding table (winner-takes-all on the UNIQUE race), append-with-dedup, /issue parser, "previous user message" fallback for bare `/issue` invocation. - Dispatcher orchestrates the inbound pipeline in one place: installation routing → group-mention filter → identity check → ensure session → append+dedup → /issue → enqueue chat task. Group sessions use the installer as creator (stable workspace identity); p2p uses the sender. Agent-offline path falls through with OutcomeAgentOffline so the WS adapter can reply with the offline notice from §4.6. - BindingTokenService: random URL-safe token, SHA-256 stored hash, 15-min TTL pinned at the application AND the DB CHECK; Redeem returns the same opaque error for all rejection cases (no timing oracle on replay). - Unit tests for the parser (13 cases), dispatcher (8 cases via fake Queries/Chat/Audit/IssueCreator/Enqueuer), and binding-token hash/entropy. Real-DB integration tests for OAuth + token redeem land alongside the HTTP handlers in the next commit. Out of scope for this commit (next ones on the same feature branch): OAuth callback, HTTP routes, WebSocket hub, outbound card patcher, frontend. Co-authored-by: multica-agent <github@multica.ai> * feat(integrations/lark): installation HTTP surface + secretbox-gated wiring (MUL-2671) Lands the HTTP boundary on top of the inbound services from the previous commit. What ships: - InstallationService.Upsert: the only path that writes lark_installation. Encrypts app_secret with the secretbox passed in at construction time; refuses to fall back to plaintext storage (returns an error from the constructor if no Box is supplied), so a misconfigured dev environment cannot accidentally land a row with cleartext credentials. Revoke flips status without DELETE so audit trail survives. - HTTP handlers under /api/workspaces/{id}/lark/: * GET /installations — member-visible (Integrations tab renders for non-admins). Soft 200 with empty list + configured:false when MULTICA_LARK_SECRET_KEY is unset, so the tab does not error on self-host that has not opted in. * POST /installations — admin-only; 503 when not configured. Re-validates agent_id ∈ workspace before accepting credentials so a cross-workspace agent UUID is rejected. * DELETE /installations/{id} — admin-only; workspace-scoped lookup so one workspace cannot revoke another's installation by UUID guess. - POST /api/lark/binding/redeem (user-scoped, no workspace context): the only path that mints a lark_user_binding row from user action. Redeemer identity comes from the session, not the token, so a stolen link cannot bind an open_id to an attacker's Multica user. The composite FK on lark_user_binding cascades the binding away if the user is not (or no longer) a workspace member, so a non-member who steals the link gets 403 at the DB layer. - Two new event-bus types in protocol.events: EventLarkInstallationCreated, EventLarkInstallationRevoked. - Router wiring: MULTICA_LARK_SECRET_KEY drives a conditional initialization of h.LarkInstallations + h.LarkBindingTokens. When unset, the integration disables itself with an INFO log and the rest of the server boots normally. - Handler tests cover all four not-configured short-circuits. Happy-path integration tests (real DB, full create→list→revoke cycle and token mint→redeem) ship alongside the WS hub PR. Co-authored-by: multica-agent <github@multica.ai> * fix(integrations/lark): close binding-token rebind & typed task errors (MUL-2671) Two must-fixes from PR review on HEAD |
||
|
|
1544e3b68a |
feat(skills): built-in agent skills (WIP) MUL-2759 (#3456)
* feat(skills): introduce built-in agent skills (WIP) Inject platform-authored, version-bundled skills into every agent on top of its workspace-bound skills, so agents learn how to operate Multica correctly without users needing to know the internals or agents needing to read source. Mechanism: skills are embedded into the server binary and appended to the agent payload at task-claim time (handler/daemon.go), reusing the existing SkillData wire + daemon-side writeSkillFiles. The daemon needs no changes, and because it travels over an existing wire field, older daemons pick the skills up the moment the server ships. First skill: multica-mentioning — how to build a working @mention (look up the UUID, match type to id source, know what each mention type triggers). WIP: injection mechanism + first skill only; more skills to follow in dependency order (skill -> agent -> squad). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(skills): make multica-mentioning the standard template + add eval Add the contract-skill frontmatter the other built-in skills will copy: user-invocable:false (it triggers from context, not as a slash command) and allowed-tools fencing it to the multica CLI it teaches. These keys survive to agent machines untouched (ensureSkillFrontmatter only ever adds a missing name). Add a Go eval in builtin_skills_test.go (a _test.go so it never ships to agent machines via the skill-files walk): - Enforces the template invariants on every built-in skill, present and future: multica- prefix, name+description present, description within 1024 chars, body within the 500-line L2 budget, no eval file leaking into the shipped payload. - Couples the mentioning skill's documented contract to the real util.ParseMentions: its Incorrect examples must parse to nothing (a name where a UUID belongs fails silently) and its Correct example must fire. A drift in the mention regex now breaks CI instead of silently turning the skill into a lie agents act on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(skills): add working-on-issues built-in skill Co-authored-by: multica-agent <github@multica.ai> * feat(skills): verify linked PRs in issue workflow skill Co-authored-by: multica-agent <github@multica.ai> * feat(skills): add skill import and discovery built-ins Co-authored-by: multica-agent <github@multica.ai> * feat(skills): add skill authoring built-in Co-authored-by: multica-agent <github@multica.ai> * docs(skills): align builtin skill workflows Co-authored-by: multica-agent <github@multica.ai> * docs(skills): use structured skill search Co-authored-by: multica-agent <github@multica.ai> * fix(skills): make built-in skill bundle launch-ready Co-authored-by: multica-agent <github@multica.ai> * fix(skills): align built-ins with additive skill binding Co-authored-by: multica-agent <github@multica.ai> * feat(skills): add creating agents built-in skill Co-authored-by: multica-agent <github@multica.ai> * Add built-in squads skill Co-authored-by: multica-agent <github@multica.ai> * refactor(skills): rewrite built-in skills as source-traced contracts Rewrite the built-in agent skills to the inbuilt-skill-authoring standard: state source-traced product facts with the source-code link logic as the core, not prescriptive how-to coaching. - creating-agents: drop the Decision-flow / Do-don't-consequences methodology; replace with field/behavior contracts (validation, persisted shape, daemon claim-time consumption, env gating, skill binding). - skill-discovery: stop teaching repo/github_stars as selection signals — searchClawHubSkills never populates them (always null); rank by install_count + source/url + description. Add file:line citations. - mentioning: drop the unbacked "member mention sends a notification" claim (no such path in the comment handler); state that only agent/squad mentions enqueue work. Tighten the parser-failure wording. - working-on-issues: refresh citations drifted by the main merge; describe the PR response `state` enum accurately; trim status coaching. - skill-importing: correct response type to SkillWithFilesResponse; document the reserved SKILL.md supporting-file rule; add line-accurate citations. - squads: correct the "leader cannot be archived" overstatement (not rejected at create/update; fails closed later at routing/dispatch); refresh source-map attributions and test list. Each skill now ships references/<skill>-source-map.md as its evidence layer (line-accurate citations live there, not pinned in the test, so a future main merge cannot rot them into stale lies). builtin_skills_test.go: replace coaching/line-number pins with drift-resistant contract anchors, forbid the coaching phrasing, and require every skill to ship its source-map. The ParseMentions behavior coupling is preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * docs(skills): close field-role and citation gaps found in review Independent review of the rewritten built-in skills surfaced two real gaps and some citation drift; this fixes them. - creating-agents: add the three missing field rows (visibility, max_concurrent_tasks, mcp_config) to the field-contract table — mcp_config is runtime-consumed (TaskAgentData, daemon.go), visibility is access-control (default private), max_concurrent_tasks is a scheduler cap (default 6). Mark custom_args/runtime_config JSON validation as CLI-side (the server marshals as-is). Correct the CLI body-builder note (description/instructions use a non-empty check, the rest use Changed). Source-map: fix the env query name (UpdateAgentCustomEnv), the conformance test name, and add the new field defaults + the McpConfig runtime-payload line. - mentioning: the @squad mention private gate is canAccessPrivateAgent, not canEnqueueSquadLeader (that wrapper is the assignment/child-done path). - working-on-issues: cite notifyParentOfChildDone at its func def (:51), not the doc comment (:15). - skill-importing: config.origin is set only when the source supplied an origin — note it may be absent; cite createSkillWithFiles at its definition (skill_create.go:72), not the call site. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * Add built-in skills for autopilots runtimes and resources Co-authored-by: multica-agent <github@multica.ai> * feat(runtime): list skill descriptions in the brief Skills index The brief's `## Skills` section emitted bare skill names only, discarding the one-line description that SkillContextForEnv already carries. For Claude-family providers the frontmatter description is loaded natively; for providers without native skill discovery (hermes/default) the brief's list is the only signal they ever see, so a bare name gave them nothing to decide when to load a skill. Emit `name — description` when a description is present, falling back to the bare name when it is empty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(skills): drop CLI-only rule from working-on-issues The "Platform data goes through the CLI" section duplicated the runtime brief's `## Important: Always Use the multica CLI` section verbatim (and the attachment-via-CLI note duplicated the brief's `## Attachments`). The CLI-only rule is universal and must be known before any skill loads, so the brief is its single source of truth; the skill copy was pure redundancy and a drift risk. Remove it and the matching intro clause. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(skills): remove discovery guidance from built-ins * docs(skills): remove stale skill-necessity records The per-skill necessity records had drifted to 3 of 8 shipped skills plus a record for `multica-skill-authoring`, which is not a shipped built-in skill. Per-skill "why it exists / when to use it" already lives co-located with each skill (frontmatter `description` + `references/<skill>-source-map.md`) where it cannot drift from the skill, and the doc's methodology duplicated the workspace's inbuilt-skill-authoring protocol. Remove the file rather than keep a parallel listing that every new skill has to remember to update. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(runtime): add source-authority escape hatch to the brief The brief already tells agents to run `--help` for command discovery, but nothing stated the trust precedence when a skill, the brief, or a doc seems to contradict actual behavior. Add one line to the Available Commands escape-hatch note: trust the live CLI (`--help`/`--output json`) and the checked-out source over source-traced prose that can lag the code, and verify on any conflict or confusion. Kept in the always-on brief (universal, needed before any skill loads) rather than duplicated into each skill; per-skill source-map pointers remain the specific layer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(runtime): scope the source-authority escape hatch to the CLI The previous version told agents the "checked-out source is the deeper authority" for verifying behavior. That over-claims: the repos in a task's brief come from GetWorkspaceRepos + project github_repo resources (per-workspace config, see daemon.registerTaskRepos), not the Multica platform source. A generic agent's checked-out source is its own app, not Multica's code, so it cannot verify a Multica skill/brief claim against it. The only universally available authority for Multica behavior is the live CLI (`--help` / `--output json` / observed command behavior). Re-scope the line accordingly and state plainly that the platform's source is not in the workdir. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * revert(runtime): drop the source-authority escape-hatch line Reverts the brief addition from |
||
|
|
9c9afd4a66 |
feat(metrics): BusinessSamplerCollector for active users / queued / runtime gauges (MUL-2947) (#3706)
* feat(metrics): scrape-time BusinessSamplerCollector for active users / queued / runtime gauges (MUL-2947)
Adds an opt-in prometheus.Collector that runs a fixed set of read-only
SQL queries on every /metrics scrape and exposes the results as gauges:
- multica_active_users{window=5m|1h|24h}
- multica_active_workspaces{window=...}
- multica_agent_task_queued{source}
- multica_agent_task_running{source,runtime_mode}
- multica_agent_task_stuck_total{source}
- multica_runtime_online{runtime_mode,provider}
- multica_runtime_heartbeat_age_seconds{runtime_mode} (histogram)
- multica_workspace_total
Plus a self-introspection histogram
multica_business_sampler_query_seconds{name=...} and a counter
multica_business_sampler_query_errors_total{name=...} so the sampler's
own behaviour is observable on /metrics.
Production-safety contract per the PR4 brief:
- every query runs in its own BEGIN READ ONLY tx with
SET LOCAL statement_timeout = '500ms' (configurable)
- the sampler takes a dedicated *pgxpool.Pool option so operators
can isolate it from business traffic
- successful results are cached for 5–10s (default 8s) to absorb
concurrent scrapes from multiple Prometheus replicas
- every SQL has a hard LIMIT 100 fallback
- all label values flow through the existing BusinessMetrics
NormalizeTaskSource / NormalizeRuntimeMode / NormalizeRuntimeProvider
whitelists, so a misbehaving runtime cannot inflate cardinality
- sampler is OPT-IN via RegistryOptions.BusinessSampler — existing
callers that only pass Pool keep their current behaviour and never
start hitting the DB on /metrics
Tests cover: emit shape, TTL cache (one DB call per N scrapes),
bounded cardinality under malicious labels, opt-out (no leakage), and
DB-hang isolation (unreachable host -> /metrics returns within 5s,
query_errors_total advances).
Refs MUL-2947 (depends on PR2 / MUL-2948, merged in #3695).
Co-authored-by: multica-agent <github@multica.ai>
* fix(metrics): address PR4 review — wire sampler in main.go, fix LIMIT bug, add live-DB statement_timeout test
Three fixes from 大彪's review on #3706:
1. main.go was building NewRegistry without the BusinessSampler option,
so the collector was effectively dead code in prod. Now constructs a
dedicated 2-conn pgxpool (newSamplerDBPool) from the same DATABASE_URL
when METRICS_ADDR is set, plumbs it into RegistryOptions.BusinessSampler,
and defers Close() at shutdown. A pool-build failure logs and disables
the sampler instead of taking down the server.
2. queryActiveUsers / queryActiveWorkspaces previously wrapped the
distinct-user/workspace subquery in a 'LIMIT 100', then COUNT(*)'d
the result — capping the active-user gauge at 100 regardless of
reality. Removed the inner LIMIT; the COUNT scalar is one row anyway,
and metric cardinality is bounded by the fixed samplerWindows
allow-list, not by the SQL shape.
3. The previous DB-hang test only exercised the acquire-fails path. Added
business_sampler_pgsleep_test.go which connects to a live Postgres
(skips cleanly when DATABASE_URL is not set), runs SELECT pg_sleep(2)
inside a sampler-style tx with SET LOCAL statement_timeout = '500ms',
and asserts:
- the call returns in well under 1.5 s (proving the server-side
cancellation, not just our caller-side context)
- query_errors_total{name=pg_sleep_canary} advances
- the duration histogram records the cancellation
Verified locally: 550 ms, SQLSTATE 57014 'canceling statement due to
statement timeout' — exactly the safety net the PR claims.
Refs MUL-2947 / PR #3706.
Co-authored-by: multica-agent <github@multica.ai>
* test(metrics): assert SQLSTATE 57014 on pg_sleep cancellation
The previous assertion only checked that the query was cut off in well
under the sleep duration, which a caller-side context cancellation
would also satisfy. Capturing the inner pgconn.PgError and asserting
Code == "57014" ("query_canceled") nails down that Postgres itself
cancelled the statement because of the SET LOCAL statement_timeout —
so a regression that drops the SET LOCAL line fails this test loudly
instead of silently passing on context cancellation.
Refs MUL-2947 / PR #3706 review nit.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
de900b2ba6 |
feat(server): funnel/community/commercial business metrics + PostHog pairing (MUL-2949) (#3698)
* feat(server): funnel/community/commercial business metrics + PostHog pairing (MUL-2949) PR3 of the Grafana board metrics split (parent MUL-2328). Adds 23 new Prometheus counter/histogram families to the PR2 BusinessMetrics collector covering the activation/community/commercial funnels, and binds every PostHog event emission to a matching metric increment so the two sides cannot drift. Funnel: signup, workspace_created, team_invite_sent/accepted, onboarding_*, cloud_waitlist_joined. Content: issue_created, chat_message_sent, agent_created, squad_created, autopilot_created, issue_executed. Runtime: runtime_registered/ready/failed/offline + ready_seconds histogram, daemon_ws_message_received_total. Autopilot: autopilot_run_started/terminal/skipped. Webhook/GitHub: webhook_delivery_total, github_event_received_total, github_pr_review_total, github_pr_merge_seconds histogram. CloudRuntime: cloudruntime_request_total + duration histogram, wired through a small RequestRecorder interface so the cloudruntime package stays decoupled from metrics. Commercial: feedback_submitted, contact_sales_submitted. The pairing helper metrics.RecordEvent(client, m, ev) emits the PostHog event AND increments the matching counter via IncForEvent dispatch, reading labels from the analytics event Properties. Every existing h.Analytics.Capture(analytics.X(...)) call site has been migrated to the helper across handler/, service/, and cmd/server/runtime_sweeper.go. Lint enforcement (server/internal/metrics/business_pairing_test.go): - TestEveryAnalyticsEventHasPrometheusCounter: every Event* constant in analytics/events.go either dispatches via IncForEvent or is in the taskMetricEvents allow-list (PR2 typed RecordTask* methods). - TestNoNakedAnalyticsCaptureInHandlersOrServices: AST-walks handler/ service/cmd-server for direct Analytics.Capture(...) calls — only service/task.go's captureTaskEvent helper is allow-listed. - TestEveryAnalyticsRecordEventTakesAnalyticsHelper: validates the third arg of every metrics.RecordEvent call is built from analytics.*. Cardinality protection: all new label values pass through fixed allow-lists in labels_pr3.go; unknown values collapse to 'other'/'unknown'/'error'. Refs: - Spec MUL-2328 / MUL-2949. - Builds on PR2 (MUL-2948) — collectors registered through the same BusinessMetrics struct, no separate Registry. - Uses PR1's taskfailure.Reason (MUL-2946) for runtime_failed's failure_reason label via NormalizeFailureReason. Out of scope: Sampler-class metrics (PR4 / MUL-2947), pr_review_total emission point (no review event handler exists yet — counter is defined, TODO to wire up when /api/webhooks/github grows pull_request_review handling). Co-authored-by: multica-agent <github@multica.ai> * fix(server): tighten PR3 review items — signup_source bucket, fill platform/kind/form_source enums, onboarding_started server emission, lint scope (MUL-2949) Addresses 张大彪's review on #3698: 1. signup_source: NormalizeSignupSource added to labels_pr3.go with a fixed allow-list bucket (direct/google/twitter/linkedin/.../other). Parses JSON cookie payload for utm_source/source/referrer fields, strips URL schemes, maps well-known hostnames to channel buckets. PostHog event still ships the raw cookie value for analytics; only the Prometheus label is bucketed. 2. Filled the unknown/other label gaps: - analytics.IssueCreated and analytics.ChatMessageSent now take a platform parameter sourced from middleware.ClientMetadataFromContext (X-Client-Platform header) at the handler. Autopilot-originated issues stamp PlatformServer. - analytics.FeedbackSubmitted now takes a kind parameter; CreateFeedback reads req.Kind (default "general") so the picker selection lights up the metric's kind label instead of long-term "other". - analytics.ContactSalesSubmitted now takes a formSource (page / onboarding / agents_page); CreateContactSales reads req.Source. The metric reads ev.Properties["form_source"] so the analytics CoreProperties.Source ("marketing_contact_sales") stays backward-compat for PostHog dashboards. 3. analytics.OnboardingStarted helper added; server-side emission lives in PatchOnboarding, fired exactly once per user on the first PATCH that carries a non-empty questionnaire payload (firstTouch logic compares prior bytes against {} / null). Frontend onboarding_started keeps firing on page open; the server emission is what guarantees the Prometheus counter exists so Grafana can be cross-checked against the PostHog funnel without depending on the SDK roundtrip. 4. business_pairing_test.go tightened: - TestNoNakedAnalyticsCaptureInHandlersOrServices now allow-lists at function granularity (just captureTaskEvent in service/task.go), not whole-file. Any future naked Capture in the same file fails CI. - TestEveryAnalyticsRecordEventTakesAnalyticsHelper now does def-use tracking inside the enclosing FuncDecl: when RecordEvent's third arg is an *ast.Ident, the test walks the function body for the assignment that defined it and confirms the RHS is an analytics.<Helper>(...) call. Bare local idents that didn't originate from analytics are now caught. 5. gofmt -w applied across the touched files; gofmt -l clean. Tests: go test ./internal/metrics/... ./internal/analytics/... pass. Pre-existing TestClaimTask_/TestWebhook_MergedPR/TestDeleteIssueByIdentifier failures on origin/main are DB-environment-dependent and not regressions from this change. Co-authored-by: multica-agent <github@multica.ai> * fix(server): normalise onboarding_started platform label + regression test (MUL-2949) Addresses 张大彪's last review nit: - IncForEvent's EventOnboardingStarted case now wraps the platform property with NormalizePlatform, matching every other platform-bearing metric. A misbehaving frontend can no longer leak a raw X-Client-Platform header value into the multica_onboarding_started_total{platform=...} series. - New labels_pr3_test.go covers every PR3 normalizer with both a happy-path value and an unknown value, asserting the unknown collapses to the documented fallback bucket. Includes a focused regression for onboarding_started: emits one event with an attacker-shaped platform string and asserts the metric only exposes web + unknown label values (no raw header bleed). - testutil.go gains a small GatherForTest helper so the regression test can pull the typed MetricFamily map without re-implementing the registry-walk dance. Co-authored-by: multica-agent <github@multica.ai> * fix(server): NormalizeTaskSource on workspace_created + document lint limitations (MUL-2949) Final review touch-ups before merge: - IncForEvent's EventWorkspaceCreated case wraps source through NormalizeTaskSource, matching the other source-bearing dispatches (issue_created, agent_created, issue_executed). Closes the last raw property leak in the dispatcher table. - business_pairing_test.go inline docstrings now spell out the two known limitations of the lint gate that 张大彪 / Eve flagged: analyticsBackedIdents matches by ident NAME (not SSA def-use, so a nested-scope shadow could pass) and isMetricsRecordEvent hard-codes the import alias set. PR description carries a Follow-ups section with the same two items so the work is visible after merge. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: 魏和尚 <agent+wei@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
24ea169d89 |
fix(migrate): serialize startup migrations with pg advisory lock (#3658)
cmd/migrate previously ran a check-then-apply loop on a *pgxpool.Pool with no locking, so two backend pods starting at the same time (multi- replica Deployment, scale-up, or a manual run overlapping with pod startup) could both pass the EXISTS check on a pending migration and race on the DDL or the schema_migrations INSERT, crashing the loser. Take a single connection from the pool, hold a session-level pg_advisory_lock for the entire migration loop, and release it on the way out. We use the blocking variant so a late arriver queues behind the current runner and then no-ops on the EXISTS checks instead of crash-looping. The loop deliberately stays outside a transaction so existing CREATE INDEX CONCURRENTLY migrations keep working. Also refresh the values.yaml / backend.yaml comments next to backend.replicas: the chart still ships replicas: 1 by default, but that is now a recommendation (Recreate strategy, no leader split), not a correctness requirement. Refs https://github.com/multica-ai/multica/issues/3647 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5900d8b637 |
fix(issues): make start_date/due_date timezone-stable calendar days (#3618) (#3692)
* fix(issues): store start_date/due_date as DATE, not timestamp (MUL-2925) These fields are calendar days (the pickers offer no time-of-day), but were stored as TIMESTAMPTZ. A client serializing local midnight via toISOString() folded its timezone into the instant, so the day shifted by the local offset (GH #3618). Migrate the columns to DATE and parse/serialize date-only "YYYY-MM-DD". ParseCalendarDate still accepts legacy RFC3339 (truncated to the UTC day) so older clients keep working. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): render start_date/due_date as timezone-stable calendar days (MUL-2925) Pickers now emit date-only "YYYY-MM-DD" (local calendar day) instead of toISOString(), and every read formats via the shared @multica/core/issues/date helpers with timeZone:"UTC" so the day never shifts with the viewer's offset. The Gantt's existing UTC bucketing is now correct. Covers web/desktop pickers, quick-set menu, list/board/detail/activity, and the mobile due-date picker. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): address date-only review — loud-fail ambiguous dates, finish display sweep (MUL-2925) Review follow-ups on #3692: - ParseCalendarDate no longer silently truncates a legacy non-midnight RFC3339 to the wrong UTC day; it accepts only YYYY-MM-DD or an exact UTC-midnight instant and rejects ambiguous ones loudly. Adds util unit tests. - migration 112 pins the TIMESTAMPTZ->DATE conversion to UTC explicitly via AT TIME ZONE 'UTC' (was session-timezone dependent); down migration too. - Convert remaining date-change display sites to formatDateOnly: inbox detail label (web) and mobile activity + inbox labels (were new Date()+local format). - CLI --start-date/--due-date help now says YYYY-MM-DD, not RFC3339. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a72fb020de |
Add business metrics collectors (#3695)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
10afd1af1b |
feat(server): introduce pkg/taskfailure classifier and switch in-flight failure_reason writes (MUL-2946) (#3693)
Lift MUL-1949's offline backfill failure_reason taxonomy into a shared in-flight classifier so the agent_task_queue.failure_reason column is written with refined values (provider_auth_or_access, context_overflow, provider_capacity_or_rate_limit, …) at write time rather than waiting on SQL backfill to re-classify after the fact. PR1 of the Grafana board plan in MUL-2328 — the upcoming PR2 reuses pkg/taskfailure.AllReasons() to pre-warm the Prometheus failure_reason label set. * server/pkg/taskfailure: new package with the canonical 21 Reason constants (7 platform-side + 14 agent_error.* sub-reasons), AllReasons() returning a defensive copy, IsAgentError() prefix check, and Classify(rawError) Reason mirroring the SQL CASE rules from MUL-1949 (db-boy's analysis). 100% statement coverage. * server/internal/daemon/daemon.go: route the 'agent_error' coarse fallback paths (StartTask error, runTask early-return error, CompleteTask permanent rejection, reportTaskResult default branch) and the executeAndDrain default error case (chained after classifyPoisonedError) through taskfailure.Classify so blocked / timeout / unknown-status results all carry a refined reason on the wire. * server/internal/service/task.go: FailTask classifies errMsg when the daemon-supplied failureReason is empty, eliminating the legacy COALESCE(.., 'agent_error') landing. * server/internal/daemon/poisoned.go: alias FailureReasonIterationLimit and FailureReasonAPIInvalidRequest to the canonical taskfailure constants. agent_fallback_message and codex_semantic_inactivity are pre-existing operational reasons not in the canonical 21 — kept as literals for now and revisited in a follow-up PR. Backfill SQL from MUL-1949 stays as the authoritative offline source of truth; this PR keeps the in-flight classifier in lock-step with the SQL CASE expression so historical and future rows share the same taxonomy. No behavior change for the platform-side reasons (queued_expired, runtime_offline, runtime_recovery, timeout, etc.) which already align with the canonical set. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f2f17e3355 |
Optimize chat message loading (#3685)
* Optimize chat message loading Co-authored-by: multica-agent <github@multica.ai> * Fix chat history cursor pagination Co-authored-by: multica-agent <github@multica.ai> * Fix chat session list remount key Co-authored-by: multica-agent <github@multica.ai> * fix(chat): fall back to legacy /messages when paged endpoint 404s Deployment-order compatibility: a backend deployed before the /messages/page endpoint existed returns 404 for the unknown route. The cursorless initial page now falls back to the legacy full-list /messages endpoint and wraps it in a single has_more:false page, so chat never white-screens regardless of which side deploys first. A 404 on a cursor request still propagates to avoid duplicating the full list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
fcb5099ec5 |
fix(agent): raise opencode model-discovery timeout to 15s (MUL-2888) (#3689)
Newer opencode (1.15+) syncs its hosted free-model catalog over the
network on `opencode models`, which can take ~6s. The previous 5s cap
killed the command, discoverOpenCodeModels returned an empty list, and
the daemon reported it as a successful empty result — so the runtime
showed online but the model picker was empty ("暂无可用模型").
Fixes #3627
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
44feb3d06d |
fix(skill): canonicalize reserved SKILL.md path check across daemon + API (#3660)
A skill_file row whose path is the skill's own SKILL.md (persisted by older builds or direct create/update API calls) collides with the primary content the daemon writes itself, failing task prep with errPathPreExists on every non-codex local runtime (#3489). #3526 guarded this with strings.EqualFold(path, "SKILL.md") at the daemon write site and the three API ingress points, but the stored path is not canonicalized: "./SKILL.md" or "sub/../SKILL.md" slip past the exact-match guard while filepath.Join still resolves them onto the same SKILL.md, so prep can still break. Extract one canonical helper, skill.IsReservedContentPath, that cleans the path before the case-insensitive compare, and use it at all four sites (execenv writeSkillFiles, skill create, update, single-file upsert). Add a daemon-side regression test for writeSkillFiles ignoring a bundled SKILL.md (exact + "./" spellings) — the load-bearing fix previously had only API-layer coverage — plus a unit test for the helper. Existing poisoned rows are intentionally left in place (skipped at prep) per the decision on MUL-2928. MUL-2928 Follow-up to #3526; supersedes #3560. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
996eb07dc5 |
fix(daemon): skip duplicate SKILL.md in supporting files to prevent task prep failures (#3526)
Fixes #3489 MUL-2928 |
||
|
|
888186b183 |
fix(daemon): make comment-posting guardrail provider-agnostic (MUL-2904) (#3654)
* fix(daemon): make comment-posting guardrail provider-agnostic (MUL-2904) Agents inlining a backtick-wrapped token into `multica issue comment add --content "..."` had the shell run it as a command substitution, silently deleting the token; the stored comment never matched the model's intent, so it retried forever — spamming OKK-497 with duplicate comments. The corruption is shell-driven, not provider-driven, so extend the "never inline --content; use --content-file / quoted-HEREDOC --content-stdin" rule from Codex-only to ALL providers: - BuildCommentReplyInstructions: collapse the Linux/macOS non-Codex inline branch into the unified quoted-HEREDOC stdin template. - buildMetaSkillContent: rename "Codex-Specific Comment Formatting" -> "Comment Formatting" and emit it for every provider; strengthen the Available Commands entry and the assignment step-6 examples to steer away from inline --content. - Windows behavior unchanged (file-only; avoids PowerShell ASCII drop). Tests: flip the non-Codex Linux reply test into a MUL-2904 regression, broaden the stdin-emphasis test across providers, and pin the provider-agnostic guardrail. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): keep Windows assignment brief file-only (address review) Review catch on #3654: the previous commit added platform-agnostic prose recommending "--content-file or --content-stdin" in the Available Commands entry and the assignment-triggered step-6 example. The assignment path has no BuildCommentReplyInstructions OS override, so on Windows an agent following step 6 literally would pipe its final comment through PowerShell and drop non-ASCII bytes (#2198 / #2236 / #2376) — contradicting this PR's own Windows file-only rule in the ## Comment Formatting section. Make the platform-agnostic surfaces defer to the OS-aware ## Comment Formatting section (the single source of truth) instead of naming stdin. The flag synopsis still lists all three modes. Add TestInjectRuntimeConfigWindowsAssignmentBriefStaysFileOnly: a Windows assignment-triggered brief must not contain any prescriptive "... or --content-stdin" recommendation. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e1a5310780 |
feat(cli): add skill content file and stdin input (#3652)
* feat(cli): add skill content file and stdin input Co-authored-by: multica-agent <github@multica.ai> * test(cli): set skill server env for flag validation Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
580ad5b492 |
fix(issues): validate and clamp limit/offset in ListIssues (MUL-2847) (#3585)
* fix(issues): validate and clamp limit/offset in ListIssues (MUL-2847)
ListIssues parsed the limit and offset query params but never validated
them, so:
- GET /api/issues?limit=-1 -> HTTP 500 (Postgres rejects negative
LIMIT with SQLSTATE 2201W)
- GET /api/issues?limit=100000000 -> unbounded read in a single
response
- GET /api/issues?offset=-1 -> same 500
SearchIssues and ListGroupedIssues already apply v > 0 + an upper clamp
on limit and v >= 0 on offset. This brings ListIssues to the same
pattern: ignore non-positive limit (keep default 100), clamp to 100,
ignore negative offset (keep default 0). default == clamp == 100 keeps
existing callers' behavior identical and matches the upstream issue
suggestion.
TestListIssues_LimitValidation seeds 3 issues in a dedicated project
and pins the nine boundary cases (negative/zero/huge/non-numeric
limit, negative/non-numeric offset, the clamp boundary, and explicit
small/positive-offset sanity) plus two sanity checks that an explicit
small limit and a positive offset are honored.
Fixes MUL-2847 / upstream multica-ai/multica#3563.
Co-authored-by: multica-agent <github@multica.ai>
* test(issues): strengthen LimitClamp test and fix comments (MUL-2847)
Address review feedback from @Lambda and @Emacs on PR #3585:
1. The 3-row set in TestListIssues_LimitValidation can't distinguish
'clamp fired' from 'clamp missing': with only 3 rows, limit=100000000
returns 3 rows whether or not the clamp exists. Split the clamp
behavior into a new TestListIssues_LimitClamp that seeds 101 issues
and asserts len(issues) == 100 for limit=100/101/200/100000000, plus
limit=50 honored below the clamp. Without the clamp line, the
huge/above-clamp subtests would fail with len == 101.
2. Fix the misleading comment that claimed 'limit=0 -> same 500'.
Postgres LIMIT 0 is valid SQL and returns zero rows. The guard
exists for sibling-consistency (SearchIssues / ListGroupedIssues
already treat v <= 0 as 'use default'), not to avoid a 500. Move
the limit=0 case out of TestListIssues_LimitValidation since it's
not 500-related; TestListIssues_LimitClamp's 'no limit returns
default page of 100' subtest pins the default behavior anyway.
3. Add a subtest that pins the offset+clamp composition
(limit=200&offset=50 against 101 rows = 51 rows), proving the
clamp caps the page size while offset still indexes the full
result set.
4. Fix gofmt: the original file's leading-bullet comment indentation
was off by two spaces; gofmt -l now reports clean.
All 14 subtests across both functions pass; full ./internal/handler/
suite still passes (3.2s).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
f539fdba83 |
feat(onboarding): backfill prompt for missing source attribution (MUL-2796) (#3550)
* feat(onboarding): backfill prompt for users missing source attribution Adds a one-shot popup shown after login to already-onboarded users whose `onboarding_questionnaire.source` was never recorded — either they completed onboarding before the source step shipped, or they clicked Skip on it. Reuses the existing 12-option StepSource UI and the existing `PATCH /api/me/onboarding` endpoint, so no schema or backend changes. Web renders it as a route at /onboarding/source (sibling of the reserved /onboarding); desktop dispatches it as a WindowOverlay per the Route categories rule. Submit and explicit Skip are terminal; the close X bumps a per-user localStorage counter and stops appearing after 3 dismissals. Emits source_backfill_shown / submitted / skipped / dismissed PostHog events so the funnel can be tracked separately from first-time onboarding. For MUL-2796. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): preserve role/use_case and respect dismiss cap in source backfill Round-2 fixes from Emacs's review of #3550: 1. PATCH wipe: `PATCH /api/me/onboarding` replaces the JSONB column wholesale (server/internal/handler/onboarding.go), so sending only the source slots was wiping role/use_case/version for exactly the historical users this targets. Read user.onboarding_questionnaire, overlay the source fields client-side via mergedQuestionnairePatch, and send the full shape. 7 unit cases cover the merge semantics. 2. Legacy single-string source: pre-multi-select rows wrote `source: "search"` as a bare string. needsSourceBackfill now treats that as already answered, matching mergeQuestionnaire (views) and stringOrSlice.UnmarshalJSON (server). Flipped the existing test and added empty-string + null coverage. 3. Dismiss cap honored in callback: the web auth callback was passing dismissCount=0, which would force-route capped users through /onboarding/source on every login (the route page would bounce them onward, but only after a blank detour and a re-fired `source_backfill_shown` event). Added readSourceBackfillDismissCount so the callback reads the same per-user localStorage bucket the prompt writes to. Test asserts a count of 3 bypasses the detour. Co-authored-by: multica-agent <github@multica.ai> * test(onboarding): clear source-backfill dismiss counter in callback test beforeEach Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): footer hint text matches the Submit button on the backfill prompt The Source step's hint reads "Hit Continue when you're ready" because its commit button is "Continue". The backfill view ships a "Submit" button instead, so the inherited hint was misleading. Add a dedicated `source_backfill.hint_ready` key across en / zh / ko and use it here. Caught during browser E2E in the round-2 verification stack. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): magic-code login also detours through source backfill The round-2 fix in PR #3550 only wired the source-backfill detour into the OAuth `/auth/callback` post-success path. Magic-code login goes through `/login` → `handleSuccess()` which calls `resolveLoggedInDestination()` and pushes directly to the workspace, so those users never reach `/onboarding/source`. Caught during the local-env demo for Jiayuan. Add `maybeSourceBackfillDetour` to the login page and apply it in both the already-authenticated useEffect and the post-verify-code handler. Predicate consults the same per-user localStorage bucket the prompt writes to, so a user who hit the close-X cap on this browser flows straight through. Co-authored-by: multica-agent <github@multica.ai> * refactor(onboarding): source backfill is a workspace-mounted modal, not a route detour Per UAT, the prompt should overlay the workspace as a Dialog with the workspace visible behind a dimmed backdrop — the original brief and reference screenshot both showed a modal. PR #3550 shipped a full-window takeover (web /onboarding/source + desktop WindowOverlay) which Jiayuan rejected. This commit replaces the full-window view with a Dialog-based `<SourceBackfillModal />` mounted once inside the shared `DashboardLayout` (packages/views/layout). The modal self-mounts: it reads `needsSourceBackfill(user, dismissCount)` and opens itself when the predicate flips to true; X / ESC / outside-click all bump the per-user localStorage cap and close. Removed: - apps/web/app/(auth)/onboarding/source/page.tsx (route) - paths.sourceBackfill (no longer needed) - callback page detour - login page maybeSourceBackfillDetour - desktop WindowOverlay type "source-backfill" - desktop navigation interception of /onboarding/source - desktop App.tsx dispatch effect - pageview-tracker case - views/onboarding `SourceBackfillView` + `readSourceBackfillDismissCount` exports Preserved (semantics unchanged): - `needsSourceBackfill` predicate (incl. legacy single-string source coercion) - `mergedQuestionnairePatch` so role / use_case survive Submit / Skip - PostHog events: source_backfill_shown / submitted / skipped / dismissed - Per-user dismiss-count cap (3) in localStorage - en / zh / ko i18n strings Tests: - 7 new tests for the modal in packages/views/onboarding/source-backfill-modal.test.tsx - Adjusted apps/web/app/auth/callback/page.test.tsx: detour tests dropped, one assertion remains that onboarded users with missing source land in the workspace (the modal handles the rest) - Full suite: 965 tests pass, typecheck + lint clean Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): mount source-backfill modal on the desktop workspace too Desktop's WorkspaceRouteLayout never wraps DashboardLayout, so the previous commit's modal mount only fired for web. Regression: desktop users were not seeing the prompt at all. Wire the same `<SourceBackfillModal />` next to `<WelcomeAfterOnboarding />` inside `workspace-route-layout.tsx`, with the matching `!overlayActive` suppression so the Dialog doesn't portal-jump above an active pre-workspace WindowOverlay (onboarding / accept-invite / new-workspace). Same component on both platforms — single source of truth lives in packages/views/onboarding/source-backfill-modal.tsx. Also drop the now-stale `source-backfill detour` comment in the web callback test fixture (Emacs nit, non-blocking). Co-authored-by: multica-agent <github@multica.ai> * test(desktop): assert workspace-route-layout mounts source-backfill modal Two structural tests pinning the round-4 fix: - `mounts SourceBackfillModal when no WindowOverlay is active` — guards against the regression Emacs caught (modal silently absent on desktop because the previous round only wired DashboardLayout). - `suppresses SourceBackfillModal while a WindowOverlay is active` — mirrors the existing `!overlayActive` rule that WelcomeAfterOnboarding already relies on so a portal-rendered Dialog can't visually outrank an active pre-workspace overlay. Mocks the SourceBackfillModal with a marker component so the test asserts mount/unmount without depending on the modal's own predicate gate. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): backfill modal Other toggles off; entrance settles after 700ms UAT round-3 follow-ups from Jiayuan: 1. **Other can't be deselected**: the modal kept a parallel `pendingOther` flag set to true on every Other click, and `IconOtherOptionCard`'s row click was guarded with `if (!selected) onSelect()` — so a second click neither flipped pendingOther nor reached the parent toggle. Drop `pendingOther` (the `source.includes("other")` derivation is already authoritative) AND add an opt-in `allowToggleOff` prop to `IconOtherOptionCard` that lets the row toggle when already selected. The text input stops click propagation so typing never deselects. 2. **Rebase + absorb GitHub channel**: rebased onto origin/main which added `social_github` (PR #3612). Modal's option list now mirrors StepSource — GitHub slotted between YouTube and Other social, reusing the existing `GitHubIcon`. 3. **Soft entrance**: defer the dialog open by 700ms after the user lands on a workspace so the underlying view paints first and the modal feels like an inviting prompt rather than a hard block. Honour `prefers-reduced-motion: reduce` (open immediately for users who have opted out of incidental motion). Tests: - New `Other toggles off on the second click instead of getting stuck` - New `renders the GitHub channel rebased from origin/main` - New `defers the entrance by ~700ms when the user has not opted into reduced motion` - Existing tests stamp `prefers-reduced-motion: reduce` in beforeEach so the dialog opens synchronously and they don't need to drive fake timers. Full suite passes (969 tests). Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): backfill modal opens reliably + Other deselects via icon area Three follow-up fixes after live UAT: 1. Strict-mode regression on entrance delay: the gate ref was being stamped when the effect *scheduled* the timer, so React Strict Mode's double-invoke cleared the first timer and then bailed on the second pass because the ref was already set, leaving the dialog forever closed. Stamp the ref only inside the timer callback (or synchronously when reduced-motion is on) so the second strict pass starts a fresh timer. 2. Other deselect: dropping `pendingOther` wasn't enough — the input that replaces the label when Other is selected was previously stopping click propagation, so a re-click on the row never reached the toggle. Remove `e.stopPropagation()` and instead let the row's onClick ignore clicks whose target IS the input (typing / focusing the input still doesn't deselect; clicks on the icon, padding, or border do). 3. Tests: drive the Other re-click via Playwright `click({position: {x:24,y:24}})` so the click lands on the icon area instead of the center of the input, matching real-user behaviour. Co-authored-by: multica-agent <github@multica.ai> * refactor(onboarding): source picker is single-select primary source Per Jiayuan's call after the survey of HDYHAU UX in PLG SaaS (Linear / Vercel / Loom / Notion / Webflow / Stripe / Figma / Cursor / PostHog mostly skip the question entirely; where it's asked the documented default — Fairing / Recast / HockeyStack / Ruler Analytics — is to capture the primary source so channel weights sum to 100% and ROI math is defensible). Modal + StepSource both pivot from multi-select to single-select radio. Server schema is intentionally untouched: `source` stays `string[]` for back-compat with v2 multi-select rows; the client always sends a one-element array. Zero migration, zero data loss. Frontend: - `source-backfill-modal.tsx`: state pivots from a multi-element `source: Source[]` to a single `pickedSlug` derived from `source[0]`; click handler replaces the array instead of toggling. Cards switch to `mode="radio"`, the fieldset gets `role="radiogroup"`, the now-redundant `pendingOther` and `allowToggleOff` opt-in go away — radio mode means no toggle-off, so the original UAT bug ("Other can't be deselected") is structurally impossible. - `step-source.tsx`: drop the `multiSelect` prop so it routes through `step-question.tsx`'s existing radio path (same one StepRole already uses). Picking a second option replaces the first; switching away from Other clears `source_other` so a stale value can't leak. - `icon-option-card.tsx`: revert the `allowToggleOff` plumbing. Tests: - `source-backfill-modal.test.tsx`: drop the multi-select toggle-off assertion; add "picking a second option replaces the first" with explicit radio-role queries. - `step-source.test.tsx`: rewrite multi-select tests as single-select (no more "stacks several picks" / "toggle off" cases); add "switching away from Other clears source_other". Full suite (970 tests) green, typecheck + lint clean. Co-authored-by: multica-agent <github@multica.ai> * docs(onboarding): refresh stale multi-select comments around source Comment-only follow-up to the single-select refactor in d14f9d09f. Five docblocks still described `source` as multi-select; they now correctly say single-select and explain the array shape is kept purely for v2 back-compat with the JSONB column. - packages/core/onboarding/types.ts — QuestionnaireAnswers docblock - packages/core/onboarding/store.ts — PostHog mirror comment - packages/views/onboarding/steps/step-question.tsx — header docblock, canContinue branch, and footer-hint comment (Source moves from the multi-select side to the single-select side; Use case stays as the remaining multi-select consumer) - server/internal/handler/onboarding.go — questionnaireAnswers docblock and the stringOrSlice fall-back comment (the column "going multi- select" is no longer the current state; rename to "pre-array shape") - server/internal/analytics/events.go — OnboardingQuestionnaireSubmitted docblock No behaviour changes. Tests + Go build still green. Co-authored-by: multica-agent <github@multica.ai> * i18n(onboarding): add ja translations for source-backfill keys The Japanese locale landed on main (PR #3538) after this branch started, so my source-backfill round-2 keys (`common.close`, `source_backfill.eyebrow / lede / submit / hint_ready`) never made it into ja and the parity test fails in CI. Add them now with translations that match the en/zh-Hans/ko wording and tone. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
91c1e51411 |
feat(editor): add / slash-command palette for invoking agent skills (#3159)
* feat(editor): add / slash-command palette for invoking agent skills Adds a `/` trigger in the chat box that opens a popover listing the active agent's skills. Selecting an item inserts a `[/label](slash://skill/<id>)` token; the daemon extracts those IDs in `buildChatPrompt` and emits an "Explicitly selected skills:" block using the canonical names from the agent's skill registry — labels are display-only and never trusted. Built on Tiptap's `Mention` extension so the suggestion lifecycle, keyboard routing, and IME handling mirror the existing `@` mention UX. Item list is sourced from the React Query workspace cache (no per-keystroke fetch). Gated behind a new `enableSlashCommands` prop so only `chat-input` opts in; other `ContentEditor` consumers (issue editor, comments) are unaffected. Read-only markdown surfaces render the token as a `.slash-command` pill via a custom link renderer + sanitize-schema/url-transform allowlists. Closes #3108 * fix(i18n): add slash_command editor copy for ko/ja The PR added slash_command popover empty-state keys to en + zh-Hans only; locales/parity.test.ts requires every locale to cover every EN key, so ko and ja failed CI. Add the two keys (no_skills_configured, no_results) matching existing skill terminology (스킬 / スキル). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2348301d2b |
fix: gate private squad leader bypass (MUL-2860) (#3648)
* fix: gate private squad leader from being triggered by unauthorized members Add canEnqueueSquadLeader helper that checks canAccessPrivateAgent before allowing a squad leader to be enqueued. Gate all EnqueueTaskForSquadLeader call sites: 1. enqueueSquadLeaderTask (comment trigger, assign trigger, backlog→todo) 2. triggerChildDoneSquad (child-done → parent squad leader) 3. autopilot.go (defensive comment; actor is always agent → always passes) Also fix validateAssigneePair's squad branch to run canAccessPrivateAgent on the squad leader, returning 403 'cannot assign to squad with private leader' when the actor lacks access. Thread actorType/actorID through notifyParentOfChildDone → dispatchParentAssigneeTrigger → triggerChildDoneSquad so the child-done path can enforce the private-leader gate. Regression tests: - Plain member blocked from create-issue to private-leader squad (403) - Plain member blocked from update-issue to private-leader squad (403) - Owner allowed to assign private-leader squad - Plain member comment on squad-assigned issue doesn't trigger private leader - Child-done by plain member doesn't trigger parent's private leader - Agent actor can still trigger private leader via comment Closes MUL-2860 Co-authored-by: multica-agent <github@multica.ai> * fix: add private-leader gate to autopilot save + dispatch paths - validateAutopilotAssignee squad branch: call canAccessPrivateAgent on the leader, returning 403 for unauthorized members at save time. - service/autopilot.go: add canCreatorAccessPrivateLeader helper that mirrors the handler-level canAccessPrivateAgent logic (agent creators pass; member creators must be owner/admin or agent owner). - Gate both dispatch paths (dispatchCreateIssue and dispatchRunOnly) with fail-closed check: if leader is private and creator lacks access, the run is skipped instead of triggering the private leader. Regression tests: - Plain member create autopilot to private-leader squad → 403 - Plain member update autopilot to private-leader squad → 403 - Owner create autopilot to private-leader squad → 201 - Owner-created autopilot dispatch → issue_created (positive) - Legacy plain-member-created autopilot dispatch → skipped (fail-closed) Co-authored-by: multica-agent <github@multica.ai> * test: add run_only legacy private-leader squad dispatch regression test Covers the dispatchRunOnly path explicitly, complementing the existing create_issue dispatch test. Both dispatch branches now have direct test coverage for the private-leader fail-closed gate. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fd1cdf1801 |
fix project progress cache invalidation (#3016)
Co-authored-by: chener <chener@M5Air.local> |
||
|
|
e36f874c86 |
feat: add additive agent skill assignment (#3642)
* feat: add additive agent skill assignment Co-authored-by: multica-agent <github@multica.ai> * test: cover cross-workspace agent skill add Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |