mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-10 10:26:46 +08:00
8ade96e671bca7d84fd2d764ffca305e2fdc358d
1469
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a4e1ea7140 |
MUL-6581: expose globally versioned Plugin Public API (#7446)
* feat(plugins): expose globally versioned public API Co-authored-by: multica-agent <[email protected]> * fix(plugins): proxy same-origin public API Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
8d2da6b8db |
MUL-6479 feat(dev): make a local environment a named object with one verb per lifecycle step (#7325)
* MUL-6479 feat(dev): make a local environment a named object with one verb per lifecycle step Local development had six entry points, thirty-four make targets and no way to see or delete what they created. `make up` / `status` / `list` / `down` / `destroy` / `gc` / `env-exec` replace that with one set of verbs, shared by humans and agents, with `C=api,web,daemon,desktop` selecting components. Three defects made the old flow untrustworthy rather than merely verbose. 1. The database the tooling created was not the one the application reached. `ensure-postgres.sh` creates through `docker exec`; when a native PostgreSQL owns 5432 the container never binds the host port, so the create lands in a server the backend never talks to. It printed "✓ PostgreSQL ready" and the next step died with `SQLSTATE 3D000`. On the machine this was written on the two servers had already diverged: 94 same-named databases, 69 more in the container with no counterpart. `up` now creates through DATABASE_URL and treats a successful `migrate up` — which pings that same string — as the only proof; failure diagnoses the port owner instead of reporting a missing database. 2. Identity was computed, not allocated. Ports, database names and CLI profiles all came from `cksum($PWD) % 1000`, and a collision was silent: the loser fails to bind while the winner keeps answering 200. Allocation now happens under a lock, probing from the same path hash so a checkout keeps its familiar numbers, and requires the slot to be both unregistered and free of a live listener. The manifest in ~/.multica/dev/envs/<name>/ is what every later command reads. 3. Creating an environment left no record of how to destroy it, so there was no destroy — only archaeology. `down` stops processes and keeps the data; `destroy` consumes the manifest: processes, database, profile, slot. `list` makes leaks visible and `gc` collects environments whose checkout is gone or whose TTL passed. /health now reports pid, commit and started_at. A 200 alone proves something is listening, which is exactly what let a failed restart keep serving the previous build; `up` compares started_at against its own launch and refuses an answer that predates it. Two constraints are enforced rather than documented: the daemon is started from a built server/bin/multica, never `go run` (the daemon re-execs its own path for every task, and `go run` deletes that binary when the launcher exits), and `C=daemon` under a .multica/daemon_task_context.json marker stops with that explanation up front instead of spending a login on a refusal. scripts/dev-env.test.sh covers the registry verbs against a throwaway MULTICA_DEV_HOME with no services running, and pins the regression that made `down` exit 1 after reporting success: on bash 3.2 a command substitution whose function ends in a failing command aborts the script under `set -e`, and "no process is listening" is that function's normal answer. `make dev` is unchanged and still runs in the foreground. Co-authored-by: multica-agent <[email protected]> * fix(dev): close local environment lifecycle gaps Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
bedad9e222 |
MUL-6485: isolate hosted plugin surfaces (#7329)
* feat(plugins): host plugin artifacts, bind installs to immutable versions A plugin used to be a URL. The manifest was frozen at install, but the surface script was fetched from the author's server every time a panel opened, with no integrity check — so an administrator consented to a manifest while the browser ran whatever that host served that day, inside the scopes already granted. The author's uptime was our uptime, every panel open leaked the reader's IP and "who read which issue" to the author, and publishing at all required a public HTTPS domain, which is why the repo's own deploy-sentinel example never ran. The author now uploads an artifact bundle and Multica stores it: - plugin_package / plugin_package_version / plugin_package_file. A version is insert-only, and the (package_id, version) unique index is what makes immutability a database rule rather than a convention. - An installation names one version. Publishing another changes nothing for a workspace until an administrator upgrades, which is a second consent. - The bundle is validated once, at publish: the manifest parses, every file it declares is present and loadable, and a surface entry with a top-level import is refused with the line named. That failure used to surface months later in a reader's browser. - The host inlines the surface script into the document it generates, so the CSP names no remote script origin at all. `connect-src 'none'` now means it: a surface with no net: scope cannot reach anywhere, including its author. - Install-by-URL is gone rather than kept alongside. Two paths would make "is this code frozen?" depend on how the plugin was installed. - MULTICA_PLUGIN_DIR stays the development channel, but publishes an ordinary immutable version instead of being a second render path; re-publishing an unchanged version lands as `+dev.N`. Hook endpoints and MCP servers remain on the author's own infrastructure. Migration 376 drops existing installations: they name a source URL that is no longer a concept, and the code they would run was never published here. plugins_v1 has never left its feature flag and holds no production data — the same call migration 344 made. Closes MUL-6469 Co-authored-by: multica-agent <[email protected]> * fix(plugins): serialize publish/install/delete, correct the isolation claim Review of #7321 found five problems. Four are fixed here; the fifth is real, needs its own design pass, and is now tracked and described honestly in the code instead of claimed away. Migration prefix collision. main took 376 for agent_task_durable_work_dir while this branch was open, so `migration prefix 376 is reused` failed backend-tests. Rebased and renumbered the six migrations to 377-382. TOCTOU between delete and install/publish. Relationships are application-owned by repository policy, so nothing made "the version this installation names still exists" true across statements: `delete counts zero installs` -> `install reads the version` -> `delete commits` -> `install commits` left an installation whose panel 404s forever, and a publish racing a delete left a version whose package row was gone. Publish, install and delete now hold a (workspace, plugin key) advisory lock for their whole transaction, and delete counts installations inside it. Both interleavings have regression tests that reproduce the bug with the lock removed. Failed publishes moved package state. upsertPackage ran before the version transaction, so a republish that lost the unique index had already renamed the package, and a first publish that failed afterwards left a plugin with zero versions. It now runs inside the same transaction. The top-level import check was wrong in BOTH directions. It read line prefixes, so it missed ` import x` and `/* c */ import x` — and it refused a valid file whose template literal contained the word at a line start, which is ordinary in a surface that renders code samples. A refusal blocks a publish with no way around it, so that false positive was the worse half. Replaced with a scanner that skips comments and string/template literals; where it is imprecise it is imprecise safely (`/` always reads as division), so a mistake can only cost a detection, never invent one. Sandbox self-navigation is NOT fixed. `sandbox="allow-scripts"` permits `_self` navigation by design, no shipped CSP directive covers it, and contentWindow is unchanged across it — so a hostile artifact can still reach its author once and hand the replacement document the bridge port. Closing it needs an embedder frame-src policy or a handshake a navigated document cannot complete; both are decisions of their own. What lands here is damage control, labelled as such: the generated document beacons on pagehide from a listener the plugin cannot detach, and the embedder drops the bridge and unmounts the frame. The "a surface cannot reach its author" wording is removed from the SDK README and the tests. Closes MUL-6469 Refs MUL-6485 Co-authored-by: multica-agent <[email protected]> * feat(plugins): isolate hosted surfaces (MUL-6485) Co-authored-by: multica-agent <[email protected]> * feat(plugins): host plugin artifacts, bind installs to immutable versions A plugin used to be a URL. The manifest was frozen at install, but the surface script was fetched from the author's server every time a panel opened, with no integrity check — so an administrator consented to a manifest while the browser ran whatever that host served that day, inside the scopes already granted. The author's uptime was our uptime, every panel open leaked the reader's IP and "who read which issue" to the author, and publishing at all required a public HTTPS domain, which is why the repo's own deploy-sentinel example never ran. The author now uploads an artifact bundle and Multica stores it: - plugin_package / plugin_package_version / plugin_package_file. A version is insert-only, and the (package_id, version) unique index is what makes immutability a database rule rather than a convention. - An installation names one version. Publishing another changes nothing for a workspace until an administrator upgrades, which is a second consent. - The bundle is validated once, at publish: the manifest parses, every file it declares is present and loadable, and a surface entry with a top-level import is refused with the line named. That failure used to surface months later in a reader's browser. - The host inlines the surface script into the document it generates, so the CSP names no remote script origin at all. `connect-src 'none'` now means it: a surface with no net: scope cannot reach anywhere, including its author. - Install-by-URL is gone rather than kept alongside. Two paths would make "is this code frozen?" depend on how the plugin was installed. - MULTICA_PLUGIN_DIR stays the development channel, but publishes an ordinary immutable version instead of being a second render path; re-publishing an unchanged version lands as `+dev.N`. Hook endpoints and MCP servers remain on the author's own infrastructure. Migration 376 drops existing installations: they name a source URL that is no longer a concept, and the code they would run was never published here. plugins_v1 has never left its feature flag and holds no production data — the same call migration 344 made. Closes MUL-6469 Co-authored-by: multica-agent <[email protected]> * fix(plugins): serialize publish/install/delete, correct the isolation claim Review of #7321 found five problems. Four are fixed here; the fifth is real, needs its own design pass, and is now tracked and described honestly in the code instead of claimed away. Migration prefix collision. main took 376 for agent_task_durable_work_dir while this branch was open, so `migration prefix 376 is reused` failed backend-tests. Rebased and renumbered the six migrations to 377-382. TOCTOU between delete and install/publish. Relationships are application-owned by repository policy, so nothing made "the version this installation names still exists" true across statements: `delete counts zero installs` -> `install reads the version` -> `delete commits` -> `install commits` left an installation whose panel 404s forever, and a publish racing a delete left a version whose package row was gone. Publish, install and delete now hold a (workspace, plugin key) advisory lock for their whole transaction, and delete counts installations inside it. Both interleavings have regression tests that reproduce the bug with the lock removed. Failed publishes moved package state. upsertPackage ran before the version transaction, so a republish that lost the unique index had already renamed the package, and a first publish that failed afterwards left a plugin with zero versions. It now runs inside the same transaction. The top-level import check was wrong in BOTH directions. It read line prefixes, so it missed ` import x` and `/* c */ import x` — and it refused a valid file whose template literal contained the word at a line start, which is ordinary in a surface that renders code samples. A refusal blocks a publish with no way around it, so that false positive was the worse half. Replaced with a scanner that skips comments and string/template literals; where it is imprecise it is imprecise safely (`/` always reads as division), so a mistake can only cost a detection, never invent one. Sandbox self-navigation is NOT fixed. `sandbox="allow-scripts"` permits `_self` navigation by design, no shipped CSP directive covers it, and contentWindow is unchanged across it — so a hostile artifact can still reach its author once and hand the replacement document the bridge port. Closing it needs an embedder frame-src policy or a handshake a navigated document cannot complete; both are decisions of their own. What lands here is damage control, labelled as such: the generated document beacons on pagehide from a listener the plugin cannot detach, and the embedder drops the bridge and unmounts the frame. The "a surface cannot reach its author" wording is removed from the SDK README and the tests. Closes MUL-6469 Refs MUL-6485 Co-authored-by: multica-agent <[email protected]> * fix(plugins): harden surface validation and error reporting * fix(migrations): renumber plugin package migrations * fix(plugins): validate classic surfaces and reset failures * fix(plugins): refresh issue-scoped surface launches Co-authored-by: multica-agent <[email protected]> * feat(plugins): isolate hosted surfaces (MUL-6485) Co-authored-by: multica-agent <[email protected]> * fix(plugins): refresh issue-scoped surface launches Co-authored-by: multica-agent <[email protected]> * fix(plugins): make surface termination event-driven Co-authored-by: multica-agent <[email protected]> * feat(plugins): isolate hosted surfaces (MUL-6485) Co-authored-by: multica-agent <[email protected]> * fix(plugins): refresh issue-scoped surface launches Co-authored-by: multica-agent <[email protected]> * fix(plugins): make surface termination event-driven Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5a80f802d3 |
MUL-6558 feat(server): make task queue expiry configurable via MULTICA_TASK_QUEUED_TTL (#7418)
Lets self-hosted deployments raise the queued-task expiry window above the built-in 2h default, so low-concurrency runtimes stop losing legitimately-pending work to queued_expired. Default behavior is unchanged. Covers .env.example, Compose, Helm values/ConfigMap, and the behavior docs in all four locales. Co-authored-by: Martin Montes <[email protected]> |
||
|
|
d60775aa93 |
docs(changelog): add v0.4.32 release entry (2026-08-21) (MUL-6541) (#7402)
* docs(changelog): add v0.4.32 release entry (2026-08-21) (MUL-6541) Co-authored-by: multica-agent <[email protected]> * docs(changelog): refine v0.4.32 release copy (MUL-6541) Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
53e15b35c6 |
MUL-6502: fix startup recovery during PostgreSQL outages (#7372)
* fix(startup): retry transient database outages Co-authored-by: multica-agent <[email protected]> * fix(startup): address database retry review Co-authored-by: multica-agent <[email protected]> * test(startup): address retry review nits Co-authored-by: multica-agent <[email protected]> * fix(startup): preserve native pgx connect timeout Co-authored-by: multica-agent <[email protected]> * fix(startup): apply timeout fallback to services Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5e2d349ec4 |
MUL-6426: refactor(dingtalk): safely retire group routing and enhance discovery (#7241)
Retire mutable DingTalk group routing: inbound messages always target the installation's configured Agent, and observed groups become read-only inventory in dedicated tables with persisted activity counters. Migrations 382-389 clean stale cross-Agent bindings, create and backfill dingtalk_group_presence and dingtalk_bot_identity, and install a mirror trigger so writes from an older process during a rollback window are not lost. The legacy dingtalk_group_route table and its indexes are retained for expand/contract; a later release drops the trigger and table together. Closes #7239 |
||
|
|
8febf40970 |
MUL-6525: docs: document GitHub App Contents permission for PR snapshots (#7369)
Add Contents: Read-only to the self-hosting GitHub App permission table in all four locales, note that it cannot be skipped along with the CI permissions, and list it in the No CI status troubleshooting entry. Co-authored-by: Sebastian San Blas <[email protected]> |
||
|
|
a3bd38da6a |
fix(cli): redact autopilot webhook credentials (#7365)
* fix(cli): redact autopilot webhook credentials * fix(cli): preserve webhook token presence metadata |
||
|
|
ff90875003 |
fix(web): only let the routed layout own the workspace singleton (#7346)
`app/[workspaceSlug]/layout.tsx` writes the platform workspace singleton from render. The App Router can keep a previous workspace's layout mounted beside the incoming one, and both re-render on their own query activity, so the unguarded write let them alternate — every render flipping the singleton back to its own slug, indefinitely. Two things read that singleton and broke with it: - `packages/core/realtime/provider.tsx` binds the realtime socket to the slug, so each flip tore the socket down and rebuilt it against the other workspace. Seen on a self-host as a `close 1005` / reconnect pair every 20-35s alternating workspace, with realtime updates lost for whichever workspace the user was actually looking at. - `packages/views/editor/extensions/mention-suggestion.tsx` reads it synchronously when the user types `@`, then looks up the agent and member caches by that id. Pointed at the other workspace those caches are cold, so the mention list came back empty. Gate the write on the live pathname. The URL is the source of truth for workspace identity and `usePathname()` returns one value shared by every mounted instance, so exactly one layout — the routed one — can define it, with no ownership handshake. Desktop keeps its own protocol (`workspace-route-layout.tsx`) because it has no URL bar to appeal to. Co-authored-by: Claude Opus 5 <[email protected]> |
||
|
|
f737974b74 |
docs(changelog): add v0.4.31 release entry (2026-08-20) (MUL-6474) (#7314)
* docs(changelog): add v0.5.0 release entry (MUL-6474) Co-authored-by: multica-agent <[email protected]> * docs(changelog): reclassify v0.4.31 release (MUL-6474) Co-authored-by: multica-agent <[email protected]> * docs(changelog): name what shipped in the v0.4.31 title (MUL-6474) The v0.4.31 title was three evaluative phrases -- "smoother agent runs", "safer automation", "reliable Windows builds" -- none of which names a thing a reader can recognize. House style for changelog titles is 2-3 parallel noun phrases naming the actual feature, page, or surface. Replace all four locales with the three changes a reader would notice: the run confirmation before an assigned Issue leaves the backlog, the Webhook event filters, and the Windows build fix. Terms follow the existing glossary and each entry body. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
814ee0cbf7 |
docs: remove six finished implementation records (#7258)
* docs: remove the two implementation docs under docs/ The database index audit and the Telegram channel review were one-off implementation records for MUL-6108 and the Telegram contribution; both are finished work and neither is referenced by the product docs. Drops the now-dangling review pointer from the Telegram handoff notes. docs/assets/ stays — the READMEs load the logos from it. Co-authored-by: multica-agent <[email protected]> * docs: remove the Telegram integration handoff notes HANDOFF-telegram.md was the implementation record for the Telegram channel contribution and is referenced by nothing. Operator-facing setup lives in the localized Telegram guides under apps/docs/content/docs/. Co-authored-by: multica-agent <[email protected]> * docs: remove three finished implementation leftovers - apps/mobile/docs/markdown-renderer-research.md: 2026-05 renderer selection research and incident log. Its conclusions already live in markdown-rendering-adr.md and in the MD_FONT/MD_LINE comments; the HIG calibration source is folded into markdown-style.ts so the numbers keep their rationale. - packages/views/locales/glossary.md: a redirect stub. The content moved to the docs-site conventions pages, which now say the file is gone instead of claiming a stub still points at them. - scripts/audit-redundant-indexes.sql: the MUL-6108 one-off audit query, orphaned once its report was deleted — no caller in Makefile, check.sh or CI. All pointers into the deleted files are updated; nothing dangles. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3268d01172 |
fix(daemon): move the in_progress start write into workflow step 3 (MUL-6417 follow-up 2) (#7295)
* fix(daemon): move the in_progress start write into workflow step 3; define ancillary by output (MUL-6417 follow-up 2) Field evidence from MUL-6460: a run that verifiably received the #7283 brief (its workdir CLAUDE.md carries the two-moment rule) walked steps 1→2→3 for over an hour and never issued a single status write. Two compounding causes, both in the prompt: - Placement. The start-moment rule lived in the status block BELOW the numbered steps. At the moment its condition triggers the model is executing the list, and a rule outside the list does not fire — the pre-MUL-6417 opening write was reliable precisely because it was an explicit step. The conditional write now opens step 3 itself, at the read→work boundary. - The research example. "A todo issue you were asked to research stays todo" was written for consult pull-ins, but the MUL-6460 turn's work WAS research toward its own issue's ask — the example pattern-matched the exact case it was never meant to cover. Ancillary is now defined by output (the turn produced none of the issue's own deliverable), and research/design explicitly count as work when they are the ask. Guard tests pin the step-3 placement and the research-counts-as-work clause, and ban the retired example. Skill, source maps, and user docs updated to match. Co-authored-by: multica-agent <[email protected]> * refactor(daemon): state the status rule as continuous, anchored at the moments that fire (MUL-6417) Bohan's review of #7295: 'at the end of your work, write the state it reached' still read as restricting writes to fixed checkpoints. The lead now states the invariant — keep status true at every point in the turn, write whenever the work changes it, mid-turn included (a blocker is recorded when hit) — and the end of the turn becomes a verification ('do not exit with a stale value') instead of the write moment. The anchors stay. A free-floating 'write when necessary' with no attachment points is exactly the shape that produced zero writes in the MUL-6460 incident: permission without a trigger moment does not fire. Step 3 keeps the start write; the exit check keeps the guarantee that a turn never ends with a lying board. Co-authored-by: multica-agent <[email protected]> * fix(daemon): drop activity-word skip-lists; ancillary is decided by output alone (MUL-6417) Elon's #7295 review: 'review' still sat in step 3's skip clause and the no-write bullet's example list, replaying the research incident's exact failure class on an issue whose ask IS reviewing a PR. Activity words carry no deliverable information in either direction, so both lists are gone: step 3 names forms of work (code, research, a design, a plan, or a review when reviewing is the ask), the no-write case is stated purely by output (the turn produced none of THIS issue's deliverable), and the rule now says so explicitly — the kind of activity never decides, only whether the output is part of what the issue asks for. Guards pin the review-as-work clause and the never-decides sentence, and ban both retired skip-lists. Skill, source map, and docs synced. Measured brief delta vs main (claude, production-length UUID): ordinary 13,362 → 13,971; leader 14,467 → 15,076 (+609 each, cached prefix); per-turn messages unchanged. Co-authored-by: multica-agent <[email protected]> * refactor(daemon): adopt J's review — one criterion, anchors in steps, category-scoped skip (MUL-6417) J's review of #7295, all four points taken: - The positive form-list that replaced the banned skip-list was the same failure shape read as exhaustive (a triage or reproduce-the-bug turn finds itself in no list). Both directions of activity-word lists are gone; the never-decides sentence moves INTO step 3 — the position that fires — instead of the status block, the position this PR itself proved dead. - The exit-side check is anchored inside step 5 ('Before exiting, ...'), making the PR's own placement argument self-consistent. - The step-3 skip is scoped to the in_progress CATEGORY, so a custom status like Planning already counts as recorded once MUL-6460 puts the status catalog in front of agents — no rewording needed then. - Tense: 'if any part of what this turn will produce ...' — step 3 is a prediction; mispredictions self-heal under the continuous rule. Plus J's meta-point on guards: presence-pins cannot tell where a sentence lives, and both field incidents were correct sentences in dead positions — the anchors are now pinned positionally (must appear inside the step-3 / step-5 lines), and the retired positive list joins the banned set. Kept the questions/discussion/acknowledgements clause (J's conservative option): acknowledgements are not covered by 'answered a question' and the sentence is the MUL-6300-era invariant other tests pin. Measured (claude, production-length UUID): ordinary brief 13,685, leader 14,790 — +243 vs main, down from +609 at the previous commit. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
cc21d19353 |
MUL-6463: confirm the promotion that starts an agent run (#7293)
* feat(issues): confirm the promotion that starts an agent run (MUL-6463) Assigning an agent to an issue opens a confirmation with a handoff note and "Don't start yet". Promoting a parked issue that ALREADY has an agent owner starts a run just as directly — the status change alone is what fires RunSourceStatus — but applied silently from a one-click picker, with no way to tell the agent anything before it woke up. The dialog now covers both writes. `RunConfirmModal` gains a `promote` mode that submits the status change (plus the same optional handoff note, or suppress_run for "Don't start yet") instead of an assignee change; the owner is already on the issue, so the promotion payload carries the status and nothing else. The gate is the run predicate, not a status allow-list: parked (backlog CATEGORY) + agent/squad owner + a target outside backlog/done/cancelled. So built-in `todo` and every custom Todo-category status behave identically, which is what the report asked for, and so do the other promotion targets that start a run the same way. Drag-and-drop and the batch toolbar keep applying directly — the existing split (MUL-4155), not a new one. Also fixes the assign gate next to it, which still compared the raw status KEY to "backlog": an issue parked in a CUSTOM backlog-category status popped a confirm promising a run the server would never start. Both gates now resolve the category the way the server does (issuestatus.Effective), preferring the payload's own status_category and falling back to the workspace catalog. Tests: promotion routing for built-in todo / custom todo / custom backlog origin / non-todo target, the five no-run status changes that stay direct, the custom-backlog assign regression, and the modal's promote payloads (note, suppress_run, no assignee fields) plus its status naming. Verified each fails without the change. Docs: the promotion dialog in assigning-issues (en/zh/ja/ko). Co-authored-by: multica-agent <[email protected]> * fix(issues): bold the status name in the promotion confirm "Move this issue to Rework." left the status looking like an ordinary word — worse for a custom status an admin named something like Later or Blocked-ish. The headline already had a fence-and-bold mechanism for the assignee name, so generalize it to bold every fenced span and fence the status too. CJK copy drops the 「」 it used for the same job: bold is now the one mechanism across all four locales. Co-authored-by: multica-agent <[email protected]> * fix(issues): close the three run-confirm gaps from review (MUL-6463) 1. Server and client disagreed about moving WITHIN the backlog category. `WillEnqueueRun`'s status branch only excluded `done`/`cancelled`, so `backlog` → a custom Backlog-category status (`Later`) changed the key, normalized both sides to `backlog`, and still enqueued a run — while the client, which reads categories, applied it silently. Before custom statuses the two conditions were the same; now leaving the category is its own requirement. Regression test covers both halves: no run inside the category, still exactly one on the way out. 2. The table's inline pickers bypassed the gate entirely — `meta.updateIssue` went straight to the surface action, so promoting an agent-owned backlog row from the table started a run with no confirmation. `meta.updateIssue` now takes the issue (the gate needs its category and owner) and routes through the same intent. 3. "Unresolvable category" was collapsing into a guess. `catalog.categoryOf` answers `todo` for a custom key it has not loaded, which made an unknown status read as not-parked and skipped the promote dialog. Category resolution is now explicitly three-state — carried, built-in, catalog, else null — and both writes that can start a run confirm on null. The gate itself moves to a pure `runConfirmIntent` next to a canonical matrix test, so the issue detail, the context menu and the table all route on one answer instead of three copies; the hook/table suites keep only their wiring. Docs nit from the same review: the new CJK paragraphs said issue / イシュー / 이슈 where the glossary requires 任务 / タスク / 태스크, and named statuses in capitalized prose instead of backticked keys. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
db0f6c5fe2 |
fix(web): keep the /favicon.ico redirect relative (MUL-6326) (#7117)
* fix(web): keep the /favicon.ico redirect relative
Response.redirect() only accepts an absolute URL, so the handler resolved
/favicon.svg against request.url. On a self-hosted deployment that carries
the server's own listen address rather than the public origin --
Dockerfile.web sets HOSTNAME=0.0.0.0 / PORT=3000, and a plain `next start`
defaults to the same -- so every client got
Location: http://0.0.0.0:3000/favicon.svg
which is unreachable, and leaks the internal bind address. The redirects
emitted by proxy.ts are already relative and were never affected.
Emit the Location header directly instead. A relative Location is valid per
RFC 7231 7.1.2 and resolves against whatever origin the client used, so it
needs no trust in Host / X-Forwarded-Host.
Closes #7116
Co-Authored-By: Claude Opus 5 <[email protected]>
* fix(web): keep /favicon.ico off the prerender cache path
Review catch on #7117. Reading `request.url` used to mark the route dynamic
implicitly; dropping that argument left nothing dynamic for Next to detect,
so it treated the handler as cacheable and tried to store a zero-byte body.
The prerender LRU rejects that outright, once per request:
Failed to update prerender cache for /favicon.ico
Error: LRUCache: calculateSize returned 0, but size must be > 0.
Every browser fetches /favicon.ico unprompted, so that is an error line per
request in every deployment, against a cache write that can never succeed.
`export const dynamic = "force-dynamic"` restores the per-request semantics
the old `request.url` read gave us by accident, without reintroducing the
absolute URL that #7116 is about.
Verified on a production build (`next build` + `next start`), before and
after. Before: `cache-control: s-maxage=31536000`, `x-nextjs-cache: MISS`,
and one calculateSize error per request. After: neither header, zero errors,
and the redirect still resolves to /favicon.svg (200 image/svg+xml).
Co-Authored-By: Claude Opus 5 <[email protected]>
---------
Co-authored-by: Claude Opus 5 <[email protected]>
|
||
|
|
cb6b0c885f |
MUL-6396: remove unused task message clipping flag (#7292)
* refactor(realtime): remove unused task message clipping flag Co-authored-by: multica-agent <[email protected]> * refactor(realtime): remove task message clipping consumers Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
a1d160ee0d |
MUL-6468: fix(docs): replace dead dimcode.ai link with official Dim CLI docs (#7294)
* fix(docs): replace dead dimcode.ai link with dimagent.cn/docs/cli The install-agent-runtime guide linked Dim to https://dimcode.ai which does not resolve. Replace with the official CLI docs at https://dimagent.cn/docs/cli across all 4 language versions (en/zh/ja/ko). * fix(docs): use English Dim CLI links for non-Chinese docs Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
49ed7f1151 |
MUL-6459: docs: document custom issue statuses (#7284)
* docs: document custom issue statuses (MUL-6459) The docs still described the seven statuses as a fixed set. Add a Custom statuses section to the Issues page in all four locales covering the model users actually need: the seven categories are fixed and each one IS its built-in status key, a custom status declares a category at creation and inherits its platform behavior in full, and that category is immutable afterwards. Also state the consequences that are easy to get wrong: board columns are categories so a new status never adds a column, built-in statuses are locked, and archiving retires a status for future assignment without touching the issues already on it. Two rules elsewhere were written as status rules but are category rules: the backlog gate on assignment and the auto-archive of run-failure inbox notifications. Both now say so. Co-authored-by: multica-agent <[email protected]> * docs: correct the run-start trigger, category scope, and key wording Review follow-up on the custom statuses section (MUL-6459). The run-start trigger was attributed to entering `todo`. The only status-change trigger is leaving the `backlog` category for anything other than `done` / `cancelled` (service/issue_trigger.go), so the rule now sits on the `backlog` row where it belongs. The claim that every status named in the docs means its category was too broad, and contradicted the system-changes callout on the same page: the failed-run rollback writes the literal `todo` key (service/task.go) and a merged PR writes the literal `done` key (handler/github.go). Scope it to statuses that TRIGGER behavior, and say the write direction is literal. Key derivation was described as always coming from the name. The API takes an explicit key and only derives one when it is omitted (handler/issue_status.go), and the key never changes on rename. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
e10287cf1b |
MUL-6457 feat(mobile): resolve custom issue statuses by category (#7286)
* feat(mobile): resolve custom issue statuses by category (MUL-6457) Mobile had no awareness of the workspace status catalog, so the custom statuses web can now define (MUL-6243) degraded badly on iOS: an issue on a custom status disappeared from My Issues and the workspace Issues list entirely, the status picker could not offer one, and filter rows rendered a raw key or nothing at all. `custom_issue_statuses` is a deployment-level flag, so creating a status on web hit iOS users of the same deployment immediately. The root cause is one mistake repeated: mobile bucketed and labelled issues by status KEY, while a status key's presentation and behavior come from its CATEGORY. A `qa` bucket existed and no section ever read it. - Fetch the catalog once per workspace (`GET /api/issue-statuses`, archived included so an issue left on an archived status keeps its real name). - Group lists by category via `groupIssuesByCategory`, extracted from the two screens that held the same fifteen lines — and the same bug — twice. Grouping reads the server-resolved `status_category`, so it never waits on the catalog and never guesses while one is in flight. - Offer `statusOptions()` in both the picker and the status filter, so a status settable in one can never be missing from the other. - Resolve name and colour through the catalog wherever a status key is shown (detail chip, search, inbox row and detail label, timeline). Built-ins keep mobile's own copy and their category token; the glyph is per category, so a custom status reads as the same kind of thing. - Keep `status_category` consistent with an optimistic status write, so the detail cache cannot feed a stale category to the recent-issues rows. A workspace with no custom statuses renders exactly as before: a built-in key is its own category, so every lookup resolves without the catalog. Co-authored-by: multica-agent <[email protected]> * fix(mobile): name custom statuses on rows and judge closed by category Addresses review on #7286. A section header names a CATEGORY, so "Code Review" and "QA" both land under In Review and the row was the only place left to tell them apart — and it said nothing. `IssueRow` now renders a `<CustomStatusChip>` next to the title, mirroring web's `list-row.tsx`. It stays silent for a status that already is its category's built-in, so a workspace with no custom statuses renders exactly as before. Covering it in `IssueRow` reaches all four surfaces at once: My Issues, the workspace Issues list, project detail, and pins. The icon-only entry points had the same identity loss with no room for a chip, so they get the catalog colour instead — `IssueRow`'s `showStatus` path (pins), the mention suggestion bar, and the mention picker. Separately, the mention bar dimmed closed issues with `status === "done"`, so an issue on a custom status in the done category rendered at full opacity as though the work were still open. It now asks the question every status-coupled rule actually asks — does this issue BEHAVE as done or cancelled — through `issueBehavesAsAny`, mirroring the core helper of the same name. Also adds the `// @vitest-environment node` pragma the repo asks for on the two new pure-logic suites. Mobile's vitest config is already node-only, so this is documentation rather than a behavior change. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4ff37edf91 |
fix(daemon): record in_progress when work on the issue's ask starts, not only at turn end (MUL-6417 follow-up) (#7283)
Post-merge report from Bohan: a todo issue assigned to an agent sat in todo for the whole first work turn — half an hour of active work the board attributed to nobody. The end-of-turn-only rule conflated when the agent can JUDGE with when the FACT changes: a turn that advances the issue's own ask makes "being worked" true the moment it begins. The activity indicator shows the run, but columns, filters, and sorting read status — the indicator alone is not the surface people watch. The status rule now has two write moments, same fact anchor: - start: the moment the turn is known to advance the issue's own ask → in_progress (only if it differs — rework on an in_review issue flips it back the same way); - end: the state the work reached (in_review / in_progress-continuing / blocked). An ancillary turn (research, answers, review, discussion) still writes nothing at either moment, so the research-stays-todo case and the concurrent-convergence property from the original MUL-6417 change are unchanged. This is the pre-MUL-6417 timing with the fact anchor's conditionality — not a revert to the unconditional lifecycle flip. Docs, skill reference, and source maps updated to the two-moment rule; guard tests pin the start moment and ban the end-only phrasing. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
8f48c380d4 |
fix(desktop): enable the renderer process sandbox (#7225)
All renderer windows ran with `sandbox: false`, so the preload script — which holds the full IPC surface exposed via contextBridge — executed in an unsandboxed context. The preload only uses sandbox-safe APIs (the `electron` module and the polyfilled `process`), so the sandbox can be turned on without touching it. The one real dependency was the build: `@electron-toolkit/preload` was externalized, and a sandboxed preload's `require` can only load `electron` plus a couple of node builtins. electron.vite.config.ts now bundles it into the single CJS preload output, which is exactly what the sandbox requires. `createRendererWebPreferences` moved out of index.ts into its own module so the security-relevant flags are pinned by a unit test; index.ts cannot be imported in tests because it registers app lifecycle handlers on import. Comments in index.ts and navigation-guard.ts that described the old `sandbox: false` state are updated. `webSecurity: false` stays off for now — restoring it requires migrating the renderer onto a privileged custom protocol so CORS preflights carry a real Origin, which needs server-side coordination first. |
||
|
|
e1be1625cb |
MUL-5959: feat(agent): add Dim (DimCode) ACP runtime (#6675)
* feat(agent): add Dim (DimCode) ACP runtime Add Dim (dimcode, the `dim` CLI) as a first-party agent runtime, driven over the ACP (Agent Client Protocol) transport via `dim acp`. Key integration points: - New dim backend (pkg/agent/dim.go): spawns `dim acp`, performs the ACP initialize/session/new handshake, and raises the runtime's hardcoded read-only permission preset to full-access (plus agent mode) via session/set_config_option before the first prompt — without it every file write is denied by a capability rule. Model override uses session/set_model; the model catalog is read from session/new. - Session resume is intentionally skipped: Dim binds sessions to the creating process, so a later process's session/load is rejected with "held by another process" even after a clean session/close. The backend always starts a fresh session and reports ResumeRejected so the daemon classifies the run correctly. A best-effort session/close keeps Dim's own session list free of orphaned entries. - Registration: SupportedTypes whitelist, New() factory, launch header, daemon probe (MULTICA_DIM_PATH / MULTICA_DIM_MODEL), default agent command name, protocol_family CHECK migration (255), model discovery, MCP config support, metrics label, and runtime display. - Docs/UI: provider logo, landing i18n (en/zh/ja/ko), providers and install-agent-runtime docs, README, CLI_AND_DAEMON, SELF_HOSTING. - Tests: dim backend unit tests (fresh session + resume behavior), shared ACP deliverable case, logo and MCP-support tests. * feat(views): use official DimCode app icon for Dim provider logo Replace the placeholder inline SVG with the official DimCode desktop client app icon (from /opt/DimAgent/resources/build/icon.png, resized to 128px), imported as a static asset the same way the Qwen Code mark is handled. * test(agent): add real dim ACP smoke tests (agentintegration) Two gated tests behind MULTICA_RUN_REAL_AGENT_SMOKE=1: - TestDimRealACPSmoke: full end-to-end against real `dim acp` — initialize, session/new, set_config_option (permission/mode), prompt, and assert completed output. - TestDimRealResumeRejected: pass a fake ResumeSessionID, assert the backend starts a fresh session (no session/load), and reports ResumeRejected=true so the daemon classifies the run correctly. Verified against dimcode 0.3.2 and 0.3.8. * fix: renumber dim migration 265 → 271 (upstream added 265-270) * fix: renumber dim migration to 272 (upstream added 271) * fix(agent): dim cross-run resume via session/load + review fixes Address review feedback on the Dim ACP runtime: - #1 Deliver runtime brief to Dim: add dim to the AGENTS.md provider list (verified dim 0.3.8+ reads AGENTS.md from the session cwd). - #2 Cross-run session continuity: dim 0.3.10+ releases its per-process session lock ~5s after the owning process exits, so resume now goes through the standard ACP session/load (same as traecli/kiro/grok) instead of always starting fresh. A loaded session retains its permission/mode, so set_config_option runs only on fresh sessions. Add a two-run regression (TestDimRealCrossRunResume) proving run B recalls context established only in run A. - #3 Process lifecycle races: the deferred cleanup now cancels the run context before cmd.Wait (a child ignoring stdin EOF no longer hangs Result delivery) with a bounded force-kill fallback; the success path waits for the final prompt notification with a grace window instead of a non-blocking read that could miss the last message. - #4 Isolate model discovery by executable path (discoveryCacheKey). - #6 Real smoke test now writes a sentinel file to prove full-access is effective; a best-effort session/close is sent before teardown. - #7 gofmt. * test(agent): add dim process-lifecycle regressions (review #3) - TestDimCleanupKillsHangingChild: a child that ignores stdin EOF and SIGTERM after an early failure is force-killed within dimProcessWaitTimeout, so Result still closes. - TestDimPromptMissingNotificationStillCompletes: when session/prompt returns without stopReason (onPromptDone never fires), the bounded final-notification wait falls through and Result completes instead of hanging. * fix(migrations): renumber dim runtime_profile migration 272 → 273 (upstream added 272) * fix(agent): review round 2 — process tree, quiescence, fail-closed, version check Address all remaining blockers from the second review: - #1 Require dim >= 0.3.10 (version check at initialize); bounded retry on session/load when the lock is not yet released instead of silently starting fresh; integration test no longer sleeps before resume. - #2 Process-tree cleanup: configureProcessGroup + signalProcessGroup + waitProcessGroupGone; cmd.Cancel returns nil so we own all signalling; regression asserts the descendant PID is gone. - #3 Notification quiescence: onActivity + waitForACPNotificationQuiescence (same as hermes); Dim equivalent of the late-final-notification test. - #4 Permission fail-closed: set_config_option runs on BOTH fresh and resumed sessions; on config failure, session/close is sent before returning so a partially configured session is not resumed; regression. - #5 Migration renumbered 273 → 274 (upstream added 273). - #6 AGENTS.md mapping regression, two-executable cache isolation test, session/close unit assertion, smoke test now executes a command. - #7 README.md + CLI_AND_DAEMON.md updated with 22 CLIs including Dim. * chore: remove temporary review actions doc (not for PR) * fix(migrations): renumber dim migration 274 → 310 (upstream added 274-309) * review: fix retry break scope, dedup isACPSessionNotFound comment, update stale notes * fix(agent): pass *exec.Cmd to signalProcessGroup/waitProcessGroupGone (upstream signature change) * fix(agent): close session on set_model failure + resume regression test Audit-found gaps from self-review: - set_model failure now sends session/close before returning (same as set_config_option failure) — reviewer #4 said 'permission, mode, or model'. - Add TestDimConfigFailThenResumeReestablishes: run A config fails → session/close sent → run B resumes and re-applies set_config_option (fail-closed), completing successfully. - Fix two stale comments that contradicted the code (config block now runs on both fresh and resumed sessions). * fix(agent): add WaitDelay, use labeled break in retry loop Self-audit improvements: - Add cmd.WaitDelay=10s for consistency with claude.go (hard backstop if a process somehow survives SIGKILL). - Use labeled break (break loadRetry) so runCtx cancellation during the retry delay exits the for loop directly, not just the select. - Update PR description: migration 273→310, set_config_option now re-applied on both fresh and resumed sessions. * fix(migrations): renumber dim migration 310 → 313 (upstream added 310-312) * fix(migrations): remove stale 313 dim migration (replaced by 314 after upstream added dsh at 313) * fix: DSH compatibility + thinking levels + reviewer round 3 Systematic fix of all 39 items from the compatibility gap analysis: Rebase + migration: - Rebase to latest upstream/main (resolves all DSH conflicts) - Migration 314_runtime_profile_add_dim (whitelist includes both dsh + dim) - Remove stale 313 dim migration DSH + Dim coexistence (provider lists, counts, docs): - SupportedTypes, config.go, metrics labels: both dsh + dim - i18n (4 files): count 23, lists include DSH + Dim + Oh-My-Pi - README.md/README.zh.md: count 23 - CLI_AND_DAEMON.md: 23-row table - environment-variables.mdx (4 langs): MULTICA_DIM_PATH/MODEL callout - display.ts, mcp-support.ts, types/agent.ts, provider-logo.tsx: both dsh+dim Thinking levels (was MISSING — dim supports thought_level): - dim.go: call applyACPEffortOption after set_model - thinking.go: add dim to acpCatalogThinkingProviders - dim.go: retain sessionResult for effort option Reviewer round 3: - Version check fail-closed: empty/malformed → reject (not allow) - Cache test through ListModels call site with fake executables - set_model failure regression test - Test companions: sidecar_manifest_test, runtime_config_test add dim Code quality: - agent.go: fix unreachable duplicate return (rebase artifact) - dim.go: labeled break in retry loop, WaitDelay, session/close on set_model fail * fix: deliverable test fake version + provider-logo rebase artifact * fix(migrations): renumber dim migration 314 → 315 (upstream added 314_workspace_mcp_config) * fix(migrations): renumber dim migration 315 → 319 (upstream added 315-318) * fix: Windows process tree, MinVersions, retry tests, migration 327 Address all 5 remaining blockers from review round 4: 1. Windows process-tree ownership: use startOwnedProcessTree instead of cmd.Start(), releaseProcessGroup in cleanup defer. This attaches the child to a Job Object on Windows (no-op on Unix), ensuring descendants are captured and terminated. 2. Migration renumbered 319 → 327 (upstream added 319-326). 3. Add dim: 0.3.10 to MinVersions in version.go so the daemon registers old Dim binaries as offline and refuses triggers (defense in depth on top of the ACP agentInfo.version check in dim.go). 4. Session-lock retry contract tests: success after bounded retries, exhaustion without falling through to session/new. Fake script supports DIM_LOAD_HELD_N (held for N calls then succeed) and DIM_LOAD_HELD_ALWAYS (always held). 5. README.md:206 20→23 runtimes. PR description updated. * test: add missing reviewer-requested regressions (round 4) - Windows Job Object regression: dim_windows_test.go proves the Dim backend captures and terminates descendants via startOwnedProcessTree (Windows-only build tag; companion to TestStartOwnedProcessTree). - MinVersions registration: add dim test cases to TestCheckMinVersion (0.3.10 ok, 0.3.9 rejected, invalid rejected). - Retry cancellation: TestDimSessionLoadRetryCancelled cancels the context during the retry delay, asserts no session/new fallback. * fix: sync upstream Command signature changes + restore dim additions Upstream changed ListModels/discoverXxxModels/detectCLIVersion to accept Command (struct with Path+Prefix) instead of string. The previous rebase kept our old-signature versions, causing widespread compile failures. Restored all affected files from upstream, then re-applied dim-specific additions: - agent.go: dim in SupportedTypes, New(), launchHeaders - hermes.go: isACPHeldByProcess + -32002 in isACPSessionNotFound - thinking.go: dim in acpCatalogThinkingProviders - models.go: dim case in ListModels + discoverDimModels - version.go: dim in MinVersions - All dim test files preserved (dim_test.go, dim_integration_test.go, dim_windows_test.go, models_test.go dim cache test) * fix: rebase to latest main, migration 327→341, resolve DetectVersion conflict * fix: remove unused os/exec import in dim_windows_test.go * fix(migrations): renumber dim migration 341 → 342 (upstream added 341) * fix: use Command.exec instead of exec.CommandContext (upstream GH #7046) Upstream's TestOnlyLaunchGoSpawnsRuntimeProcesses requires all backends to build processes through Command.exec (launch.go), so a custom runtime's fixed_args are carried into the subprocess. Dim was the only backend still using exec.CommandContext directly. * fix: launch prefix policy + remove empty test files + deterministic retry tests Address all 4 findings from review round 5: 1. Add "dim": dimBlockedArgs to launchPrefixBlockedArgs so protocol-breaking flags (--help/--auth-setup/--remote) are filtered from fixed_args. 2. Remove 77 zero-byte *_test.go files accidentally added at repo root. 3. Make retry tests deterministic: TestDimSessionLoadRetryCancelled waits for the first session/load request before cancelling (no fixed sleep); TestDimSessionLoadRetryExhausted asserts exactly 4 load attempts. 4. PR description will be updated separately. * fix: rebase to latest main, migration 342→343 (upstream added mcode at 342) * chore: trigger CI * chore: refresh PR * fix: restore mcode that was lost during merge conflict resolution Merge took 'ours' side which predated mcode. Restore mcode in: - SupportedTypes, New() factory, launchHeaders - agent_supported_types_test.go want map - metrics/labels.go - config.go defaultAgentCommandNames - migration 343 up/down whitelists * fix: restore mcode probe in agents_probe.go + agent-cli-command-names.txt * fix: restore all remaining mcode references lost during merge Files fixed: - README.md: mcode row in runtimes table - mcp-support.test.ts: mcode assertion - config.go: mcode in Agents comment + error message - runtime_config.go: mcode in AGENTS.md case - version.go: mcode in MinVersions - version_test.go: mcode test cases Verified: no file has fewer mcode references than upstream. * fix: restore mcode in environment-variables docs + CLI_AND_DAEMON ACP list - environment-variables.{mdx,ja,ko,zh}: restored 'MiniMax Code' in the QwenPaw model-variable sentence - CLI_AND_DAEMON.md: added MiniMax Code to ACP-family list + loadSession fallback description Verified: every changed file now has >= mcode references vs upstream. * fix(migrations): renumber dim migration 343 → 344 (upstream added 343) * fix(migrations): renumber dim migration 344 → 348 (upstream added 344-347 plugin migrations) * fix(migrations): update down migration comment 344 → 348 * chore: retrigger CI (flaky TestOpenclawDiscoveryCacheConcurrentPreparations — upstream test, passes locally x5) * fix(migrations): renumber dim migration 348 → 352 (upstream added 348-351) * fix: Dim launch-prefix regression test + version.go comment repair (review #6) - Add TestDimLaunchPrefixFiltersBlockedFlags: proves allowed prefix reaches command before acp, and --help/--auth-setup/--remote/-h are stripped from Dim's launch prefix. - Add dim to TestLaunchPrefixReachesACPFamilies family list. - Repair version.go: dim's cross-run session/load comment was appended to mcode entry during a rebase conflict; restore two independent accurate comments. * fix(migrations): update down migration comment 348 → 352 * chore: retrigger CI (backend-tests stuck 50+ min) * fix(migrations): renumber dim migration 352 → 362 (upstream added 352-361) * fix: use logAgentCommand for dim argv logging (upstream redaction requirement) Upstream's TestOnlyLaunchGoLogsAgentCommandArgs enforces that all runtime argv logging goes through Config.logAgentCommand so sensitive args are redacted. dim.go was still using Logger.Info directly. * fix(migrations): renumber dim migration to 370 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
658b0b7d9f |
MUL-6406: fix(auth): fail fast in production on insecure JWT secret defaults (#7212)
* fix(auth): fail fast in production on insecure JWT secret defaults * chore(deploy): require a strong JWT_SECRET in compose and docs * fix(ci): make selfhost config test pass with required JWT_SECRET The compose file's JWT_SECRET:? error message contained a colon-space sequence that YAML parsed as a mapping indicator, breaking docker compose config. Quote the openssl hint instead. .env.example now ships an empty JWT_SECRET, so the test's docker compose config calls fail interpolation; supply a throwaway secret from the environment, which Compose lets outrank the env file. * fix(ci): seed JWT_SECRET into the recipe .env in selfhost config test The Makefile includes .env and bare-exports every variable to the recipe environment, so the make-driven recipe cases clobbered the test's JWT_SECRET export with the empty value .env.example ships. The stub's `docker compose config` then failed interpolation with stderr discarded, produced empty JSON, and crashed the node parser. Seed the throwaway secret into the recipe .env, which both make and Compose load. * docs(env): use a CSPRNG for the Windows JWT_SECRET generation hint Get-Random is not cryptographically secure, so the Windows hint could leave self-hosters with a predictable signing key. Point at RandomNumberGenerator instead, per review. * docs(selfhost): fix stale JWT secret comments Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4f56785b02 |
refactor(daemon): merge Reply/Ownership turn modes into one fact-driven workflow (MUL-6417) (#7245)
* refactor(daemon): merge Reply/Ownership turn modes into one fact-driven workflow (MUL-6417)
The Reply/Ownership split dated from PR #205 and had already been merged
in substance by MUL-5377 (one brief section + mode router) and MUL-6300
(reply turns own the same status arc). What remained was a per-turn
marker plus two mode blocks restating information the per-turn message
already carries as data. This finishes the merge:
- Status is judged once, at the END of the turn, from what the work
actually changed about the issue — not from the trigger type, not from
the run lifecycle, and not gated on being the assignee. No opening
in_progress write: the board's in-flight signal is the issue activity
indicator, and a todo issue the agent was only asked to research now
correctly stays todo (the old unconditional arc got that wrong twice).
Lifecycle writes oscillate under concurrent runs; fact writes converge,
which is what the retired assignee gate existed to protect.
- Delivery routes on data: a triggering comment in the per-turn message
(reply in that thread with THIS turn's --parent, one reply per thread)
or none (post a new top-level comment).
- The blocked rule and the squad-leader dispatch case become end states
of the same judgment instead of mode-block special cases; the leader
keeps one bullet (dispatching members is not delivery). The reply-mode
mention pointer is gone — the discipline lives in ## Mentions.
- The Turn mode markers are removed from the per-turn prompts, deleting
the marker/router-drift failure class (
|
||
|
|
f7abcdded3 |
docs: publish the community-maintained areas and their maintainers (#7249)
Until now, "community-maintained" appeared in the docs without ever naming who maintains what, and the actual maintainers were recorded only in Go package comments and PR threads. Users filing an issue had no way to learn any of it. Add a Community-maintained areas page in all four locales listing each area, its code path, its maintainers, and the retirement rule, and make that page the single source of truth: channels, the DingTalk and Telegram guides, the README, and the three package doc comments now point at it instead of restating it. Those package comments no longer carry maintainer handles or a second copy of the policy, so ownership changes are recorded in exactly one place. The page is deliberately a credit-and-expectations page, not a support directory — it asks readers to file a GitHub issue rather than page a volunteer directly, since what we promised these maintainers is that we would route work to them, not that they are on the hook for it. It also avoids the term "code owner": we are not adding a GitHub CODEOWNERS file, and that term carries a specific automated meaning there. Both issue forms gain a required Area field so a report can be routed without asking the reporter to read source, with the community-maintained areas marked as such in the option list. Co-authored-by: multica-agent <[email protected]> |
||
|
|
67c6aa05c4 |
MUL-6166: feat(telegram): add Telegram channel integration (#6944)
* feat(telegram): add Telegram channel integration Co-authored-by: multica-agent <[email protected]> * chore(telegram): rebase onto main and renumber migrations to 366-367 349/350 collided with main's agent_task_queue_chat_terminal_resume_index and agent_task_queue_chat_retired_session_index. Renumber the Telegram origin pair to the next free prefixes, update the cross-references inside the SQL comments, and point telegram_origin_migration_test.go at the new filenames. Contributor-facing behavior is unchanged. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> Co-authored-by: Bohan-J <[email protected]> |
||
|
|
801c671973 |
chore(assets): convert bitmaps to WebP, dedupe, and gate image size (MUL-6352) (#7149)
* chore(assets): convert web and docs bitmaps to WebP The repo carried 21.7MB of PNG/JPG and zero modern formats. next/image already serves visitors AVIF/WebP, but the raw masters are still paid for on every clone, every Docker build context, and every deploy upload. Re-encode with cwebp and cap the source width at the largest size any layout can request: 1920px for the decorative landing backgrounds (they render behind content under object-cover), 2640px for the landing hero (2x its 1320px slot), 1600px for screenshots (2x the ~720px docs and use-case column). EXIF goes with it — the four feature backgrounds were 144dpi print exports. Encoder choice is per image rather than uniform. Most compress best as lossy; two of the docs screenshots are flat palettized PNGs that encode smaller with `-lossless -z 9`, so they use that instead. Screenshots are q80 to keep UI text crisp, decorative art q72. apps/web/public 10.70MB -> 1.57MB apps/docs/public 7.47MB -> 2.44MB Verified: 1:1 crops against losslessly-downscaled references show no visible difference; all 266 image references in the tree resolve; docs prerenders all 177 pages and the landing/use-case pages render with no broken images. Co-authored-by: multica-agent <[email protected]> * chore(assets): drop the duplicated README hero image docs/assets/hero-board.png was byte-for-byte identical to the docs site's workspace-overview screenshot — 633KB stored twice. Only the two READMEs referenced the copy, so point them at the canonical file instead. Co-authored-by: multica-agent <[email protected]> * chore(assets): losslessly recompress app icons oxipng -o max --strip safe over the app icon PNGs: -640KB with pixels untouched (verified by comparing lossless-WebP encodes of the before and after of every file — all 16 identical). Deliberately not switched to WebP and not regenerated: the prerendered sizes under apps/desktop/build/icons/ exist because electron-builder's auto-derivation silently shipped only the 1024x1024 source in the v0.2.31 .deb (#2515), and the PWA manifest and .icns/.ico containers want PNG. Filenames, dimensions, and the electron-builder config are unchanged, so packaging behaves exactly as before. Co-authored-by: multica-agent <[email protected]> * ci: gate pull requests on a 300KB image budget Keeps the previous three commits from being undone one screenshot at a time. A bitmap added — or grown — past 300KB fails the check until the PR description carries an `Oversized image exemption:` line saying why. Shrinking an oversized file always passes: the check compares against the size at the base ref, not just the absolute size. Path-filtered to PRs that touch a bitmap, so the full-history checkout it needs to diff against the base is only paid for when it is relevant. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
0c0baf0505 |
perf(realtime): stop the task:message fanout from flooding every client (MUL-6396) (#7227)
* perf(realtime): stop the task:message fanout from flooding every client (MUL-6396) `task:message` is broadcast to every client in the workspace for every run, and the client cached all of it: the handler's `(old = [])` default built a timeline entry on first sight, so each client accumulated the full transcript of runs its user would never open, held for the life of the run. Tool input is unbounded on the wire, so a single large Write shipped its whole body to every open client and was retained there. Three changes, each useful on its own: - Client drops frames for tasks no mounted view is rendering. Presence in the query cache is the signal, which registers when a view mounts and before its fetch resolves, so a frame landing mid-backfill is still kept. Rows are persisted before they are broadcast, so a dropped frame costs nothing — whoever opens the task next fetches it. - Server clips oversized `input` / `output` out of the broadcast copy and marks it `truncated`. Persisted rows and the REST list responses are untouched. A client seeing the flag repairs that task's timeline from the DB, once per flush window rather than once per frame. - Client coalesces surviving frames into one cache write per 100ms window, turning a burst of streamed messages into one merge and one render. The window is fixed, not a resetting debounce, so a continuous stream still flushes instead of being starved until it pauses. Mobile keeps its own per-session updater and was already scoped to mounted sessions, but shares the wire format, so it refetches on `truncated` too. The remaining half of the batching idea — one WS frame per reported batch instead of one per message — is deliberately not here. The client identifies itself on the upgrade URL but the server does not retain that version, so a shape change would silently break every older client. It belongs with the client scope-subscription work that `listeners.go` is already waiting on. Co-authored-by: multica-agent <[email protected]> * fix(realtime): keep broadcast clipping off the wire, and stop a late fetch dropping live seqs Addresses both must-fix findings from review. 1. Clipping the broadcast copy is not something an existing client can ignore: it changes what `input` / `output` mean, and every deployed client already consumes them. A build without the `truncated` reader caches the clipped copy under staleTime:Infinity and never refetches, so its execution log would stay incomplete until the window is reloaded — the PR even proposed shipping the server half first, which is exactly backwards. Clipping now sits behind MULTICA_CLIP_TASK_MESSAGE_BROADCAST, off by default, so the reader ships first and the flip is a later, revertible ops step. Capability routing would be the principled fix, but the hub does not retain the client_version it is handed at upgrade and frames cross nodes through the Redis relay as serialized bytes — the same protocol work per-task scope routing is already waiting on. 2. Not caching unopened tasks made first open of a live task fetch where it previously found a pre-seeded cache. That fetch replaced the array wholesale, so any frame written while it was in flight was dropped — and with staleTime:Infinity nothing would ever fetch it again. Every write to the timeline cache now folds through one rule, installed as the query's own structuralSharing: union by seq, server data winning on conflict, rows the response never mentioned kept rather than deleted. That subsumes the backfill's hand-rolled eviction, so the three write paths (fetch, backfill, realtime batch) can no longer disagree. Also documents why mobile's truncated branch is unreachable today: its task:message handler gates on chat_session_id, which the server's payload does not carry, so mobile appends nothing at all. Pre-existing, reported separately, and the branch stays so whichever side is fixed first is not left caching clipped rows. Co-authored-by: multica-agent <[email protected]> * fix(realtime): re-check the timeline is still held before writing a batch Holding was verified when a frame entered the batch, but the write happens up to a window later — and `setQueryData` does not postpone garbage collection. query-core arms that timer when the last observer leaves and never re-arms it on write, so closing a transcript while its run keeps streaming has the entry collected mid-window with a batch already in flight. Writing then rebuilt the entry holding only that batch. Under the app-wide `staleTime: Infinity` the next open reads the stub as fresh and never fetches, so everything before it is missing until the window is reloaded — silent, permanent, and not self-healing. Agent runs routinely outlive the 10 minute gcTime, so "opened a running task, closed it, it kept going" reaches this on an ordinary day rather than in a race. The flush now re-checks immediately before each write and drops the batch if the entry is gone, dropping its repair fetch with it. `backfillTaskMessages` guards its own post-await write for the same reason. Dropping costs nothing: the rows are persisted, so the next open fetches the whole timeline — which the regression test asserts rather than just checking the entry stays absent. Also corrects the doc comment on isTaskMessageTimelineHeld, which described the entry as a mounted view. It means "opened at some point and not yet collected", and the bounded tail is one gcTime. Co-authored-by: multica-agent <[email protected]> * docs(realtime): describe the task:message gate as it actually behaves The overview still said frames are dropped unless a mounted view holds the task, and that the old behaviour retained a transcript for the life of the run. Neither matches the code: the gate is timeline-entry presence (opened at some point, not yet collected), and writes do not postpone GC, so retention is bounded by gcTime rather than the run. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9bd556785f |
fix(inbox): give the desktop shell navigation feedback when switching issues (#7223)
Switching rows in the desktop inbox had no loading affordance at all: the click froze the window until the new issue detail committed, then everything appeared at once. Web looked smooth for the same work because it shows a progress bar over the swap. Two reasons, both fixed here: - `NavigationProgress` only ever shipped inside web's `DashboardLayout`. Desktop builds its shell from `desktop-layout.tsx` and never mounted the component, so the bar simply did not exist there. It now sits at the top of `MainCanvas`, desktop's equivalent of web's `SidebarInset`. - `useIsNavigating()` tracked only the adapter's push/replace transition. On desktop those are synchronous store writes, so the flag settles before the destination content has rendered — the bar would have flashed and gone. Views can now report an in-page swap through `useReportNavigating()`, which feeds the same signal. The inbox uses both. Its detail pane's selection is deferred, so mounting `IssueDetail` — the expensive half of a switch — runs at transition priority instead of blocking the click: the row highlight and the URL commit immediately, the previous issue stays painted, and the gap is reported to the progress bar. `resolveDetailItem` holds that stale row only while it is still in the list; archive drops the actioned row in the same commit that advances the selection, and holding the miss would blink the empty state between the two issues. Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d563bfbc08 |
docs(changelog): add v0.4.30 release entry (2026-08-19) (MUL-6403) (#7221)
Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c2474e2fe1 |
fix(agents): align private-agent invoke surfaces with the owner-only contract (MUL-6380) (#7202)
* fix(mobile): gate the chat agent picker on the real invoke rule (MUL-6380) apps/mobile/lib/can-assign-agent.ts was a hand-copied mirror of the pre-MUL-3963 permission model and had drifted from the server in both directions: - it granted workspace owner/admin a bypass on private agents, so an admin could pick a teammate's personal agent in the chat picker and only learn it was refused when the send hit canInvokeAgent (403); - it judged on the derived `visibility` field, which collapses "public_to scoped to specific people" into "private" — so a member the owner had explicitly granted access to never saw that agent on mobile at all. Delete the mirror and call `canAssignAgentToIssue` from @multica/core/permissions directly, the same rule the issue mention bar on this app already uses (mention-suggestion-bar.tsx). The root CLAUDE.md sharing rule allows mobile to import pure functions from @multica/core, so the mirror bought nothing but a second copy to drift. No new tests: the access matrix stays canonical in packages/core/permissions/rules.test.ts, which already covers the admin, owner, plain-member and member-target cases. Co-authored-by: multica-agent <[email protected]> * fix(agents): stop telling users workspace admins can run a personal agent (MUL-6380) Since MUL-3963 the invoke gate is owner-only for a private agent — admins keep management and view access, not the ability to trigger a run. Several strings still describe the old model, and they sit next to controls that enforce the new one: the "Personal" badge tooltip on the agent detail page, overview summary and hover card says "only you and workspace admins can use this agent", while the assignee picker beside it is disabled and reads "only the owner can assign work". - packages/core/agents/visibility-label.ts: fix VISIBILITY_DESCRIPTION and VISIBILITY_TOOLTIP (shown in the create-agent dialog, the visibility picker and the agents-list lock tooltip). The description covers a CHOICE, which submits permission_mode "private" — owner-only, so "Only you can assign". The tooltip covers an EXISTING agent, where `visibility: "private"` is the lossy projection that also covers "shared with specific people" — so it names the owner's grants instead of promising either extreme. The doc comments asserted the opposite contract and cited a long-stale line ref. - packages/views/locales/{en,zh-Hans,ja,ko}/agents.json: same fix for the VisibilityBadge tooltip, which is also its aria-label. Co-authored-by: multica-agent <[email protected]> * fix(issues): stop naming the target's permission mode when an assign is denied (MUL-6380) validateAssigneePair answered a refused assignment with "cannot assign to private agent" / "cannot assign to squad with private leader". Both are wrong twice over: - they disclose the target's permission mode to a caller who was just told they may not use it, which is exactly what ReasonInvocationNotAllowed (dispatch/reason.go) deliberately avoids — it does not distinguish "not permitted" from "does not exist"; - "private agent" is simply inaccurate for a `public_to` agent scoped to specific people, which is refused through the same branch. Report the missing permission instead of the target's configuration. Co-authored-by: multica-agent <[email protected]> * fix(issues): correct the authorization contract stated at the assignment gate (MUL-6380) Review found the drift this PR set out to fix still present in the comments and test docs closest to the gate itself: - validateAssigneePair's doc comment claimed assignment runs canAccessPrivateAgent and that agent callers bypass the gate. It runs canInvokeAgent, and an agent caller is judged by the top-of-chain human originator like anyone else — the code twenty lines below has said so since MUL-3963. - TestShouldEnqueueOnComment_PrivateAgentGate's doc listed "allow workspace owners/admins" and "allow agent-to-agent traffic regardless of visibility"; its own cases assert both are DENIED. - TestCreateIssue_SquadPrivateLeader_OwnerAllowed's doc said a workspace owner can assign; the body uses the leader agent's owner, and an inline comment already corrected the mismatch. - The comment added earlier in this PR overclaimed: it read as if the 403 were enumeration-safe, but the not-in-workspace branch still answers 400. Narrowed to the guarantee actually made — the reason no longer names the permission mode. Also pins the denial BODY, which no test covered: status-only assertions let the reason sentence drift back into describing the target. New cases cover the workspace owner, a plain member, an agent actor with no originator, a `public_to` agent scoped to someone else (the case the old wording called "private", which was simply false), and a private-leader squad. Co-authored-by: multica-agent <[email protected]> * fix(chat): make a session read-only when its agent can no longer be run (MUL-6380) Filtering the agent PICKER was only half the path. A chat session outlives the permission that created it: the agent can be flipped to personal, change owner, or drop the member from its allow-list. The server already treats that as a defined state — the transcript stays readable through the view gate while every send is refused by the invoke gate (MUL-4525, admission_security_mul4525_test.go) — but the clients kept resolving the open session's agent from the full list and never re-checked whether it could still be run. The composer looked live, and the user found out from a 403. Judge the SESSION's agent, not just the picker list, on every surface: - packages/views: `isAgentAccessRevoked` in use-chat-controller.ts and chat-window.tsx, wired into the composer disable, upload affordance, quick actions, queued send-now, and a new AgentAccessRevokedBanner. handleSend keeps a belt-and-braces guard so it never spends a roundtrip on a certain refusal. - apps/mobile: same derivation in the chat tab, plus the failure path — a thrown send only restored the draft, so the 403 read as a silent no-op. It now names the cause, via a mobile-local `dispatchReasonCode` (core's narrows on core's ApiError class, which mobile never throws). Two reason-precedence fixes fall out of this. "No agents in this workspace" was shown when the workspace's ONLY agent was the revoked one — an agent IS bound, it just cannot be run — so revoked access now wins in the placeholder, the banner and mobile's disabled reason. And the queue's Steer button said "Steer is available after the current reply starts", telling the user to wait for something waiting cannot fix; it now states the permission reason. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3c3e77b00c |
MUL-6362: redact secrets from provider command logs (#7206)
* fix(agent): redact provider command values Co-authored-by: multica-agent <[email protected]> * fix(agent): preserve safe command diagnostics Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
24d8c521a6 |
MUL-6364: make the LLM client retry budget explicit, wired, and validated (#7201)
* feat(llm): make the LLM client retry budget explicit, wired, and validated Config.MaxRetries was a bare int that nothing outside tests ever set. The production wiring in handler.New never passed it and no MULTICA_LLM_* env var fed it, so the field was configurable in name only and every deployment ran on the SDK default. Its semantics were also inverted. Because the zero value and an explicit 0 are the same int, "0" fell into the unset branch and silently produced 2 retries, while a negative — clamped to 0 to dodge the panic in option.WithMaxRetries — was the only value that actually disabled retries. - MaxRetries becomes *int so unset, 0 (disabled) and N (exact) stay distinct. New always passes the budget to the SDK explicitly, including the default, so the enforced policy and the reported one cannot drift apart. - Add MULTICA_LLM_MAX_RETRIES as the single configuration source. Non-numeric, negative and above-ceiling values fail the boot instead of being corrected into something that looks configured. The ceiling of 5 is a latency budget: backoff reaches ~21s at 6 retries while the internal callers of this layer time out after 8s and 20s. - Report the effective policy at startup via Client.RetryBudget, whose fields are counts and a fixed enum so the line cannot carry a key or gateway URL. - Document the four states in .env.example and the environment-variable docs, and record what the budget does not cover: the GenerateJSON parameter negotiation and a stream that breaks after ChatStream returns. Tests pin every state by upstream request count, so the unset-versus-zero collapse cannot come back unnoticed. Refs MUL-6364, closes #7154 Co-authored-by: multica-agent <[email protected]> * fix(llm): reject invalid retry budgets at the client boundary and wire self-host Review found the retry budget still reachable in two invalid shapes, and never reachable at all on the main self-hosted deployment path. - docker-compose.selfhost.yml declares the backend environment as an explicit allowlist and mapped none of the MULTICA_LLM_* variables, so every value .env.example and the docs told a self-hoster to set was dropped before the container: the LLM layer read as unconfigured no matter what they wrote. Map all four, and assert propagation in scripts/selfhost-config.test.sh, plus a drift guard so a future MULTICA_LLM_* documented without a mapping fails the script instead of silently going nowhere. - Config.MaxRetries took a *int, so a negative still reached llm.New, which warned and used the default instead. Warning is not silence, but substituting a different budget is still the implicit correction #7154 asked to remove. It now takes *RetryOverride, whose field is unexported and whose only constructor rejects negatives — the invalid state is unrepresentable, so New has no correction branch left to take. - The budget is a ceiling, not a quota: only retryable failures consume it, and a success or the caller's deadline can end a call sooner. Say "at most N" rather than "exactly N" in the field docs, .env.example and all four environment-variable docs. The negative-value test now asserts rejection instead of pinning the fallback as contract. Refs MUL-6364, closes #7154 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
bb8a04f3a1 |
docs(llm): disclose what the server-side LLM layer sends, and test the zero-request opt-out (MUL-6369) (#7199)
* docs(llm): spell out what the server-side LLM layer sends upstream (MUL-6369) The two MULTICA_LLM_* variables are the only switch governing whether chat content reaches an external model, but nothing said so: the .env.example comment named chat titles as an example consumer, the docs page said only that callers 'use local fallback logic', and neither said what is sent. An operator whose policy forbids chat content leaving the deployment had no way to confirm that leaving the variables empty is sufficient, which is what GitHub issue #7162 reports. Document the layer where it is configured: both consumers (chat auto-titling, follow-up questions) and exactly what each sends, plus the fact that leaving the key and base URL empty is a supported configuration that costs nothing but the LLM-written title and the suggestion buttons. The package doc carries the same inventory next to the code, so a new consumer is added in the file that documents them. Co-authored-by: multica-agent <[email protected]> * test(llm): pin the zero-upstream-request and single-entry-point contracts (MUL-6369) The documentation added in the previous commit makes two promises that were true only by accident. An unconfigured client returning ErrNotConfigured was never asserted to do so without dialing, and 'every server-side LLM call goes through pkg/llm' held only because no one had imported the SDK elsewhere yet. TestUnconfiguredClientMakesZeroUpstreamRequests drives every exported call path through a transport that counts and refuses, then proves the counter can move for a configured client so the zero-count assertions cannot pass vacuously if the HTTPClient seam is ever dropped. TestOutboundLLMTrafficGoesThroughThisPackage parses the server module and fails on an OpenAI SDK import outside this package, because the consumer inventory is only auditable while this package is the only door. Co-authored-by: multica-agent <[email protected]> * fix(llm): scope the privacy claim, and guard the consumer inventory (MUL-6369) Addresses both must-fixes from review. They land together because they share the same surfaces: the package doc is the inventory the operator copy is written from, and it names the tests that hold each half. 1. The disclosure claimed more than the code guarantees. The copy said these variables are the deployment's only switch for sending chat content to a model, and that emptying them means the server sends nothing to any provider. What is actually guaranteed is narrower: server/pkg/llm makes no request. Running an agent reaches a model by an entirely separate path — the daemon executes an AI coding tool under that tool's own credentials, with MULTICA_* stripped from its environment. For a privacy document that gap matters more than it would anywhere else: an admin who reads the old wording concludes emptying two variables stops all model traffic, which is the kind of wrong belief #7162 is about. Every surface now says "this layer sends nothing" and names the agent path explicitly — .env.example, the four docs locales, the package doc, the test comments. 2. The inventory had no guard. The import test proved the OpenAI SDK has one door; it said nothing about how many callers walk through it, so a third feature calling GenerateText would ship undisclosed with every test green while the docs kept naming two consumers. TestDocumentedConsumersAreTheOnlyCallers scans production call sites and requires the set to match the published list in both directions: an undocumented caller fails, and so does a documented one that stopped calling, since an inventory that overstates what is sent misleads too. The import test is renamed TestOpenAISDKIsImportedOnlyByThisPackage to claim only what it checks. Also drops the package doc's stale "drafting a quick-create issue" example, which named a consumer that does not exist and contradicted the inventory two paragraphs below it. Co-authored-by: multica-agent <[email protected]> * fix(llm): correct the MULTICA_* claim about agent environments (MUL-6369) Review caught a factual overreach in the previous fix. The copy said MULTICA_* variables are dropped from the agent's environment rather than passed to it, which is not what mergeEnv does: it drops the namespace the daemon INHERITED, then appends the task-scoped values daemon.go assembled — MULTICA_SERVER_URL and MULTICA_TOKEN reach the child by design, and claude_test.go asserts it. The guarantee an operator actually needs is narrower and still sufficient: the daemon does not forward these MULTICA_LLM_* settings to the AI coding tool, and agent runs use that tool's own credentials. Say that instead, and name the task-scoped injection so the sentence does not contradict the subprocess env contract. Applied to .env.example, the four docs locales and the package doc. Also splits methodNameCollisions out of documentedConsumers, per the same review's non-blocking note. A method-name collision quieted by adding a file to documentedConsumers would have corrupted a list operators read as 'what sends my chat content somewhere' in order to silence a test — a bad trade the guard should not invite. Empty today; the repository has no collisions. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4b8873df3a |
feat(issues): add revision-aware concurrency guards (#7038)
* feat(issues): add revision-aware concurrency guards * fix(concurrency): decouple text edits from aggregate revisions |
||
|
|
c5f00bbf5c |
docs(mobile): document EXPO_APPLE_TEAM_ID for multi-team signing (MUL-6375) (#7196)
#7173 added the ios.appleTeamId mapping but left it undiscoverable: the people it helps are stuck on a signing error, not reading app.config.ts. Document it the same way its sibling EXPO_BUNDLE_IDENTIFIER_* overrides are — README, .env.example, and a why-comment at the call site. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
44ba031eeb |
fix(mobile): configure Apple team from environment (MUL-6375) (#7173)
Map EXPO_APPLE_TEAM_ID to Expo's ios.appleTeamId so self-builders with multiple Apple Developer teams can pin the signing team. Unset keeps Expo's existing auto-detection behaviour. Closes #7172 |
||
|
|
41ddf6c0a1 |
refactor(frontend): drop dead UI components and phantom deps, add knip (MUL-6353) (#7147)
* refactor(frontend): drop dead UI components and phantom deps, add knip `.npmrc` sets shamefully-hoist=true, which hides both halves of dependency drift: an undeclared import still resolves, and an obsolete declaration still installs. Neither build, typecheck, nor lint reads the import graph, so package.json stopped being the source of truth for what is actually used. Removed 16 zero-reference components from packages/ui (1,907 lines, all `pnpm ui:add` output that never got wired up) plus mention-hover-card.tsx, which was written but never imported — member and agent mentions render as a plain `.mention` span. The comment in issue-hover-card.tsx claiming otherwise was stale; corrected to match rich-content.tsx. Deleting pagination.tsx orphaned its two i18n keys, dropped from all four locales and from the `ui` namespace type. Dependency declarations removed where nothing imports them: - vaul, embla-carousel-react — only the deleted drawer/carousel used them - date-fns, @tanstack/react-query-devtools — zero imports repo-wide - motion, tw-animate-css from packages/ui — real users are views/desktop/web - 44 of apps/web's 58 declarations, including all 15 @tiptap/* entries. One had already drifted: apps/web pinned tailwind-merge ^3.5.0 against the catalog's ^3.4.0, a second source of truth for the same package. Added @types/hast to packages/views, which imports it directly. knip enforces this going forward, scoped to files/dependencies/unlisted — `exports` is unusable here because ui and views are consumed through per-file exports maps. The CI step runs in warn mode (continue-on-error) and reports 9 pre-existing findings left out of this cleanup; clear those before promoting it to blocking. Verified: build, typecheck, lint green across web/desktop/ui/core/views. 6,503 tests pass; the 9 failures are pre-existing on a clean tree (a local jsdom localStorage issue). Web dev server boots and serves /login 200. Co-authored-by: multica-agent <[email protected]> * fix(ci): cover the packages/ui blind spot knip cannot see Review caught that knip provided no regression protection for the exact thing the preceding cleanup was about. knip derives entry points from a workspace's package.json `exports` — graph/build.js calls `getEntrySpecifiersFromManifest` unconditionally, with no config flag to opt out — and packages/ui exports four wildcards. Each expands to a glob over its whole directory, so every file under components/ui, components/common, markdown, and hooks registers as an entry point and can never be reported unused. A zero-reference probe component confirmed it: knip's output was byte-identical with and without it. `entry: []`, negated `entry` patterns, `--production`, and `--strict` were all tried. Manifest entries come from a separate code path that none of them subtract from. packages/views is unaffected because 44 of its 45 exports name a specific file, which is why knip does flag dead files there. Added scripts/check-ui-wildcard-exports.mjs to cover those four directories, blocking from the start since it reports nothing once the one file it found is gone. It reads the wildcards from the manifest, so narrowing an export hands that directory back to knip without the two overlapping. Its test asserts all three states: clean tree passes, a stranded component fails and is named, and importing that component clears it again. The check found packages/ui/hooks/use-auto-scroll.ts — 80 lines whose only occurrence repo-wide was its own definition. Same category as the 16 components already removed, and invisible to knip for the same reason. Also from review: - The path filter missed knip.jsonc and apps/docs/**, so config-only and docs-only PRs skipped knip even though it analyzes docs. - The knip step paired `continue-on-error` with `|| true`, which zeroed the exit code before Actions saw it — no warning surfaced, only log text. Dropping `|| true` makes the warn phase visible and the later promotion to blocking a one-line change. - Corrected the knip.jsonc comment that claimed apps were the only entry points for all three shared packages. True for views and core, never true for packages/ui. Verified: build, typecheck, lint green (12/12, 0 errors). 6,503 tests pass; the same 9 pre-existing failures as before, reproduced on a clean tree. Co-authored-by: multica-agent <[email protected]> * fix(ci): resolve import paths in the ui export check instead of matching names Review found the check passed a dead component whenever any file elsewhere in the repo imported a same-named module. The relative-import branch was a whole-repo text regex on the basename, so `../components/option-card` in packages/views vouched for an untouched packages/ui/components/ui/option-card. That is not a corner case: 17 of the 59 guarded files already share a basename with another file in the repo — `button`, `card`, `input`, `label`, `avatar`, `tabs`, `switch`, `skeleton`, `separator`, `theme-provider` among them. shadcn names are generic by construction, so the hole covered the most likely names a future `pnpm ui:add` would produce. Specifiers are now resolved against each importing file's own directory and compared against real paths, with `@multica/ui/...` mapped through the same manifest wildcards the candidate list is built from. Liveness is now reachability from outside the guarded directories rather than a reference count, so two dead components importing only each other no longer vouch for one another. Also implements the exemption the failure message promises: a file named by a non-wildcard export is an entry point in its own right and is skipped. The candidate loop previously read the wildcard directories unconditionally, so following that advice would not have cleared the check. Tests cover all five states: clean tree, stranded component named, cleared by a real import, the option-card basename collision, and a mutually-importing dead pair reported as two files. Verified: check and its tests pass, knip unchanged at 8 files + 1 dependency, build/typecheck/lint 12/12 green. Co-authored-by: multica-agent <[email protected]> * fix(ci): parse imports with the TypeScript parser, not a source regex Review found the scanner counted commented-out imports as real edges. It ran a regex over raw source, so a component kept its liveness from a line that does not execute — and "delete the last real import, leave the comment behind" is a normal step in a refactor, which makes the false edge appear at exactly the moment the component stops being used. Edges now come from real syntax nodes: static import/export declarations, `import()`, `require()`, and `import("x")` type nodes. `vi.mock("...")` is excluded on purpose — it names a module without importing it, so a component whose only remaining mention is a test mock is dead, and counting it would let one outlive its last real use. Scope, measured rather than assumed: 52 specifiers in this repo appear only in comments or prose, spread over 43 files, so the mechanism is live. None of them currently resolve into packages/ui, so no dead component is being missed today — this closes a latent hole, not an active miss. (An earlier count of 3 was wrong: those were `lazy(() => import(...))` calls that my throwaway measurement script failed to treat as real.) Test gains a sixth state: a component referenced only by a commented-out import and a string containing one must still be reported. Verified: six-state test passes, clean tree passes in 0.9s over 2,089 files, knip unchanged at 8 files + 1 dependency, build/typecheck/lint 12/12 green. `typescript` is already a root devDependency, so no new declaration. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
e3a40b0b8d |
feat(MUL-6243): custom issue status UI — picker, board, filters, settings (#7103)
* refactor(views): widen IssueStatus to open key, resolve category at render sites Board columns, swimlane columns and hidden-column panels are CATEGORIES, not status keys, so adding a custom status never adds a column. Retyped those to IssueStatusCategory and made every presentation lookup (STATUS_CONFIG, i18n labels, status icons) resolve the key to its category first. Status filters stay in concrete keys and map back to columns via the catalog. MUL-6243 Co-authored-by: multica-agent <[email protected]> * feat(views): catalog-driven status picker and per-status labels The picker now offers the workspace catalog's active statuses grouped by category, instead of the 7 hardcoded built-ins, and falls back to the built-ins until the catalog lands so a cold render is unchanged. Built-in names always resolve through i18n, never through the catalog's server-seeded English name — otherwise every non-English workspace would see 'In Progress' where it used to see '进行中'. Custom names come from the catalog. That rule lives in one place: useStatusLabel. Applied it to every surface that shows an issue's OWN status (detail activity feed, table group headers and CSV export, hover card, inbox), which previously showed the category label and so could not tell two statuses in the same category apart. MUL-6243 Co-authored-by: multica-agent <[email protected]> * feat(views): show custom statuses on cards and in filters Board columns and list sections are categories, so two issues in the same column can sit on different statuses with nothing to tell them apart. CustomStatusChip is that signal, and it renders nothing for a built-in — a workspace with no custom statuses looks unchanged. The status filter now lists the workspace catalog instead of the 7 hardcoded built-ins. Without this an issue moved onto a custom status could not be narrowed to at all. Picker and filter share one useStatusOptions hook so the two lists cannot drift apart. MUL-6243 Co-authored-by: multica-agent <[email protected]> * feat(settings): issue status catalog management with drag reorder Owner/admin CRUD for custom statuses, organised by CATEGORY rather than as one flat list. Category is not decoration here — it is the behavior a status inherits, and picking it is the one moment an admin can, since it is immutable afterwards. Built-ins are shown but locked: each is its category's canonical definition. Drag reorder writes intra-category positions starting at 1, because the built-in is seeded at 0 and PATCH on it is a 403, so it always heads its category. The optimistic cache write re-SORTS rather than only writing positions, or the drag would visually undo itself until the refetch. custom_issue_statuses is now published to the frontend, so the tab can hide creation when the deployment has the rollout gate off instead of offering a button that 403s. MUL-6243 Co-authored-by: multica-agent <[email protected]> * fix(issues): close the category contract end to end Elon's review found the board only half-converted: the UI targeted categories while the server still paged by concrete status key, so a card on a custom status was dropped twice — it never came back from the server, and the row filter would have discarded it if it had. 1. New `status_category` group kind (and compound secondary axis). Grouping rewrites the column with a static CASE built from the catalog; paging expands the category to concrete keys and stays on `status = ANY(...)`, so (workspace_id, status) is still usable. A workspace with no custom statuses gets exactly `i.status` back. Row filter, facet totals and swimlane cells all fold by category now. 2. Column visibility split from the status filter. Hiding a column used to write the other six built-in keys into statusFilters, so hiding Backlog silently dropped every CUSTOM status in other categories. Display state is now its own field. The saved-view baseline also stopped deleting custom status filters on reopen. 3. The board/list right-click menu reads the shared catalog options, so every entry point that can set a status offers the same set. 4. Remaining presentation consumers resolve a category instead of guessing: gantt bars were keyed by IssueStatus and went colorless on any custom key; hover card, sidebar, search, tab strip, inbox row, create toast and the activity feed all resolve explicitly now. 5. Reorder is one atomic statement behind a new endpoint, and the client submits ACTIVE rows only. The per-row PATCH loop half-committed when an archived row rejected mid-sequence. Also: one catalog observer per card instead of three, and the stale tasks/todo.md draft is gone. MUL-6243 Co-authored-by: multica-agent <[email protected]> * fix(issues): atomic reorder, category behavior sweep, rollout-safe grouping Second review round. 1. Reorder is now one transaction holding the catalog's SHARED lock. The old shape validated ids, THEN ran the UPDATE — an archive committing in that window left the UPDATE silently skipping the archived row while the earlier rows were rewritten, and still returned 200. The authoritative active set is read under the lock and the payload must match it EXACTLY: a partial order would assign positions from the array index and collide with the rows it left out. Affected rows are checked against the payload before commit. 2. Swept the Web behavior consumers that still compared raw keys, so a custom category actually inherits behavior: gantt hide-completed, batch backlog parking, done/cancelled strikethrough, the mark-done button, sub-issue progress, search cancelled-demotion and mention demotion/dimming. issueBehavesAs is the one predicate they share, and it fails toward showing more and asking more. Two glyph sites (gantt row, swimlane lane) were still painting custom in_review as Todo. 3. Rollout safety. status_category is a contract this PR introduces, and the default-off creation flag does not guard the request path, so a new Web build would have 400'd against any un-upgraded pod — for every workspace. The client now sends it only once the catalog confirms the workspace HAS a custom status, which can only be true if the fleet already serves this version. Everyone else keeps the exact request they made before. That also uncovered a real defect: plain status grouping 500'd on a workspace holding custom statuses, because the descriptor rejected any non-built-in value and failed the whole response. 4. One catalog read per resolve instead of three, and the common path reads none at all. Pinned with observable scan-count tests rather than an assertion that the SQL string matches. 5. A custom-key filter no longer routes on a guess. categoryOf answers todo for anything unloaded, so a saved qa filter paged the todo column while the query still restricted status=qa — empty on every cold load, permanent if the catalog fetch failed. Unresolved custom keys now contribute no column until the catalog is authoritative; facet counts drop unknown keys rather than inflating todo. MUL-6243 Co-authored-by: multica-agent <[email protected]> * fix(issues): swimlane category axis, catalog tri-state, tests with teeth Third review round. 1. Swimlane was only half-converted. The compound spec sent secondary=status_category unconditionally — the same rolling-deploy hole the board/list branches had — and the client row matcher then compared the raw key, so a qa row the server correctly folded into the in_review cell was thrown away again. Both now follow the same hasCustomStatuses switch, and the matcher knows which axis it is on. 2. The cold-load fix did not actually make the surface wait. Returning an empty column set is indistinguishable from "narrow to nothing", so the surface fetched nothing and rendered an empty board with no spinner; on catalog failure that was permanent, with no retry. Resolution is now pending / resolved / error: pending and error suspend fetching AND hold loading, error surfaces isStatusCatalogError. Fetching is suspended rather than left to fall back to all columns, which would briefly show the UNFILTERED board to someone opening a saved qa view. The group contract also joins the branch cursor identity, so a status cursor cannot ride into a status_category request. 3. Two tests were false-green and are replaced: - The reorder race archived BEFORE calling reorder, so the active-set check caught it even with the lock deleted. It now reproduces the real interleaving (reorder parked on the lock, archive commits, then reorder proceeds) and fails without the shared lock — with a 200, which is the actual hole. - The catalog read count read pg_stat_user_tables, whose counters flush asynchronously, and asserted delta <= 1 — vacuous at zero. It now counts through an injected querier and asserts exactly 1 read and 0 category-expansion reads. Also: plain status grouping 500'd on a workspace holding custom statuses (the descriptor rejected non-built-in values and failed the whole response) — that is the path the Table view and every client fallback use. And the chat mention items dropped status_category, so a custom done status in Current/Recent rendered as an active Todo. MUL-6243 Co-authored-by: multica-agent <[email protected]> * fix(issues): give the status catalog failure a real user path Fourth review round. One group, both halves of it. A failed status catalog suspends row fetching, because a custom status filter cannot be routed to a column without it. That left isLoading and isEmpty both false, so IssueSurface fell straight through to the content branch and rendered an empty view with no explanation and no way out — the controller's isStatusCatalogError had no consumer. The catalog now carries its own `retry` (React Query's refetch) rather than just a failure flag, the controller re-exposes it, and IssueSurface renders an explicit error state ahead of loading/empty/content. Component test covers the full loop: catalog rejects, error appears with no row requests issued, clicking retry succeeds, fetching resumes. The blocking condition is also narrowed. A BACKGROUND refetch can fail while the last successful catalog is still cached, and error-before-loaded meant that perfectly serviceable snapshot got a retry screen in front of it. Only a failure with no data behind it blocks now; otherwise the last successful catalog keeps resolving. That rule lives in status-behavior.test.ts, where it can be asserted directly. The component test asserts the wiring — it was originally written to claim the rule too, but reverting the narrowing did not fail it, so the claim moved to the layer that actually holds it. MUL-6243 Co-authored-by: multica-agent <[email protected]> * fix(issues): accept custom status keys in table filters.statuses Filtering a board, list or table by a custom status 400'd the whole request: `filters.statuses` carries the exact keys the user picked, and the table query still validated that list against the 7 built-ins. Every surface errored out instead of narrowing — the one thing the filter work in this PR exists to do. The predicate is a parameterized `status = ANY(...)`, so an unknown key simply matches nothing; the check is now non-empty and length-bounded. Found while writing the post-deploy verification steps, which is where the gap became obvious: "filter by QA" was step one and it 400s. MUL-6243 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4d5a679b9b |
fix(openclaw): unblock the OpenClaw runtime on slow hosts (MUL-6321) (#7142)
Fixes #7112. Task preparation ran `openclaw config file` and `config get agents.list --json` with a hardcoded 5s deadline; on a 2020 Intel MacBook Pro those take 8.4-10.9s and 4.3s every time, so the OpenClaw runtime was unusable there with no way to raise the limit. - Deadline 5s -> 30s, overridable via MULTICA_OPENCLAW_CLI_TIMEOUT / backends.openclaw.cli_timeout, clamped to [1s, 60s] so the worst case (4 serial calls) stays inside the 5-minute preparation budget. - Config discovery cached per daemon profile (on disk, since preparation runs in a one-shot helper process), invalidated by binary identity, OpenClaw path env, active config stat, and a 60s TTL. Only complete successes are cached; any cache fault degrades to a live CLI run. - The timeout is classified structurally into a new platform-side, non-retryable reason (runtime_cli_timeout) instead of being text-matched into agent_error.provider_network, which showed "check your network" copy and auto-retried a deterministic local failure. Copy added for web/desktop/mobile; a NormalizeDaemonReason shim upgrades reports from older daemons. |
||
|
|
c670e05490 |
docs(changelog): add v0.4.29 release entry (2026-08-18) (MUL-6355) (#7145)
* docs(changelog): add v0.4.29 release entry (2026-08-18) (MUL-6355) Co-authored-by: multica-agent <[email protected]> * docs(changelog): refine v0.4.29 release title Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
1b6d42864f |
feat(desktop): reland open Settings with Cmd/Ctrl+, (MUL-6233) (#7076)
Reverts the revert (#7079) of #7022, on top of the tab-remount fix in the previous commit — the bug that made the feature look broken (MUL-6293) lived in the tab host's workspace-context lifecycle, not in the chord. Reverting the chord only stopped the fastest way to reach it; switching tabs took the same path. Feature is unchanged from #7022: Cmd+, (Ctrl+, on Windows/Linux) matched in the main process alongside the other app-owned accelerators so it fires from any window and any focus context, routed to the tabbed main window through the pending-message queue, opening-or-activating a Settings tab rather than navigating in place. Mod+, joins the reserved keys in the shared shortcut registry. Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
22db0b0a2c |
MUL-5651: preserve tasks through runtime network partitions (#7129)
* fix(runtime): preserve tasks through network partitions Co-authored-by: multica-agent <[email protected]> * fix(runtime): bound reconnect retry waiting Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
f78c867168 |
docs(tasks): describe the execution log where it actually lives (#7120)
The 'Viewing run history' section read as a two-step navigation ('go to the
Execution log' then 'open a run record'), which does not match the product:
ExecutionLogSection renders inline in the issue's right sidebar, expanded by
default, with a one-click View transcript on each row.
An external report (#7107) asked for an inline collapsible execution view on
the issue page based on this wording, and separately asked whether issue
pages auto-refresh. Both already exist; the docs described neither.
- Say the log lives in the issue's right sidebar, not a separate page/tab
- Note the default-expanded state, pinned active runs, Show past runs
- Point at the sidebar panel button for when the section is not visible
- State that the log updates live over the realtime connection
- Mention the header live chip and the transcript's Newest first toggle
- Add the sidebar screenshot ahead of the existing transcript screenshot
Applied to en/zh/ja/ko.
Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
de902a4aab |
MUL-6273: feat(agent): add MiniMax Code ACP runtime (#7035)
* feat(agent): add MiniMax Code ACP runtime * fix(agent): terminate mcode process trees * fix(agent): address mcode review blockers * test(agent): keep mcode model contract API-stable * fix(migrations): resolve mcode prefix collision * fix(agent): launch mcode through runtime command Assisted-by: MiniMax Code * fix(migrations): renumber mcode migration Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
51ae68d8b9 |
fix(web): show the newest fully-uploaded release on /download (MUL-6313) (#7102)
The /download page rendered every Windows and Linux button as disabled. v0.4.28's release run had its Windows desktop packaging job fail and its Linux job never finish, so the release carried Mac builds only — and the page reads whatever the newest release happens to be. Release selection guarded against this with a time proxy: fall back to the previous release for the first hour after publish, on the assumption that a gap means uploads are still in flight. That only covers the mid-flight case. A release whose packaging job failed outright never completes, so once the hour elapsed the page pinned itself to a release it could not fully serve. Select on the actual signal instead: parse each of the recent releases and show the newest whose desktop asset set is complete, so every button on the page resolves to a real file. Widen the candidate window from 2 to 5 and log when the newest release is skipped, since a release that never completes is otherwise invisible until someone reports dead buttons. Completeness now has one definition, `hasCompleteAssetSet`, shared with the "view all releases" fallback link that had counted assets inline. Verified against live GitHub data: v0.4.28 is skipped and v0.4.27 serves all twelve artifacts. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
38c992ad0a |
docs(changelog): add v0.4.28 release entry (2026-08-17) (MUL-6305) (#7093)
Covers the 7 PRs merged onto main after the v0.4.27 tag: member actor / multi_actor custom property types (#7075), bulk skill update from source (#6930), the desktop changelog link on the update prompt (#7088), and the same-workspace tab swap fix (#7092). Bumps apps/web to 0.4.28. Patch bump: per-workspace custom issue statuses (#7065) is the largest change in the window but ships behind the default-off custom_issue_statuses rollout flag, so nothing user-facing today is announcement-grade. Test-infrastructure work (#7081, #7072) is intentionally absent from the changelog: it changes no user-visible behavior. Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
f66b0ba631 |
fix(desktop): keep the workspace singleton across a same-workspace tab swap (MUL-6303) (#7092)
Opening or switching a tab inside one workspace left the desktop client with no active workspace: the sidebar vanished, every workspace-scoped request went out without `X-Workspace-Slug` and came back 400, and nothing recovered it short of relaunching the app (#7086). Desktop mounts exactly one tab at a time and keys the host on the active tab id, so a tab swap unmounts the whole router subtree and builds a new one beside it. React renders the incoming tree before running the outgoing tree's effect cleanups, and the release path added in #7064 guarded only on slug equality. That guard sees a workspace SWITCH, but not a swap within one workspace: the "+" button opens /<slug>/issues in the active workspace, so the incoming layout carries the same slug, its render write dedupes to a no-op inside setCurrentWorkspace, and the outgoing cleanup then finds getCurrentSlug() still equal to its own slug and clears it. The incoming tab's queries fire from effects, which run after that cleanup, so its very first requests are the ones that go out unscoped. Give each mounted layout an instance identity and have it claim the singleton in the same render pass that writes it. The unmount release now bails when a successor already owns the singleton. The slug check stays: it still covers the same instance re-running for a new slug, where the singleton has moved on. Tests mount the real keyed-host swap rather than calling unmount() by hand, since the bug is entirely about React's commit ordering, and assert on the slug a child query observes — that value is what became the 400s. The platform mock now mirrors the real slug-equality early return, without which the incoming layout's write would look like it could have saved the singleton. Co-authored-by: Multica J <[email protected]> |
||
|
|
21e77f3625 |
fix(desktop): link update prompt to changelog (#7088)
Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9a30c51dac |
MUL-6286: feat(properties): actor / multi_actor custom property types (members) (#7075)
* feat(properties): add actor / multi_actor custom property types (MUL-6286) Custom issue properties gain their first reference type: `actor` (single) and `multi_actor` (multiple), holding workspace members and agents. Value shape is a prefixed string — "member:<user_id>" / "agent:<agent_id>", multi being an array of them. Keeping it a plain string rather than an object means the existing @> containment filter, the jsonb_path_ops GIN index and the client value schema all keep working unchanged; an object-shaped value would have been silently dropped by the client preprocess that guards against unknown value shapes. Scope decisions: - Squads are out. The assignee pair accepts member/agent/squad, but a squad is a routing target rather than a person. The stored form carries the kind, so widening later needs no migration and no new type. - Referencing an agent is NOT an assignment: nothing is enqueued, routed or notified. The picker therefore does not gate agents on runtime binding or invoke permission the way the assignee picker does — it gates on visibility, matching the server. - Agent identity still needs protecting, so writes and facet reads both run through accessibleAgentIDs: a private agent never becomes referenceable or discoverable by id. - multi_actor dedupes but preserves insertion order instead of canonicalizing like multi_select. There is no config order to sort against, and sorting by id would reshuffle the avatar row on every edit. @> containment is order-insensitive, so filtering is unaffected. - Actor properties are filterable (including "= me", with the signed-in member sorted first) but not sortable or groupable — the same position multi_select already holds. Both types ship together because a property's type is immutable: shipping single-only would have forced anyone wanting multi to recreate the property and re-enter its values. Co-authored-by: multica-agent <[email protected]> * refactor(properties): narrow actor properties to members only (MUL-6286) Scope change after review: `actor` / `multi_actor` hold workspace members only. Agents are out — a passive reference had to answer agent-visibility questions (private / non-allow-listed agents must not become discoverable by id) and carry a "referencing is not assigning" caveat everywhere, all for a use case nobody has asked for. The original request was for members. The envelope is unchanged on purpose, so this stays a one-line widening later rather than a migration: the types are still named `actor` / `multi_actor`, the stored value is still the kind-prefixed "member:<user_id>", and the migration still allows both type names. Adding "agent" back means appending to `actorPropertyKinds` — existing definitions would gain the kind in place, with no new property type and no value rewrite. What this removes: - The agent branch of `resolveActorRefs`, including the accessibleAgentIDs visibility gate and the archived-agent check. - `filterAccessibleActorFacetValues` entirely. Facet keys are now all `member:<uuid>`, and workspace membership is already visible to every member, so there is nothing left to gate. - The agent section of the actor picker, and agents from the filter dropdown's candidate list. - The "reference, not an assignment" framing from the skill and product docs — that distinction only existed because agents were allowed, and keeping it would now confuse rather than clarify. `actorPropertyKinds` carries the note that any future kind which is not universally visible to every member must restore the visibility gate on both the write path and the facet read path. Tests updated to match: agent refs are now rejected at the kind check, and the private-agent visibility test is replaced by TestIssueActorPropertyFacets, which covers the facet path that backs the "= me" filter. Co-authored-by: multica-agent <[email protected]> * fix(properties): address actor-property review findings (MUL-6286) Four defects from review, plus one pre-existing bug found next to them. 1. Editing a multi_actor value silently deleted kinds the client did not recognise. The picker rebuilt the array from `actorRefsFromValue`, which filters unknown kinds — so once the backend widens actorPropertyKinds, an installed desktop build would drop every agent reference the moment the user toggled a member. The edit path now round-trips the RAW string list via the new `actorRefValuesFromValue`, and the toggle is extracted to `toggleActorRefValue` so the guarantee is directly testable. 2. Actor ids were stored exactly as received. `uuid.Parse` also accepts uppercase, braces and the urn: form, and every consumer (member directory lookup, "= me" filter, @> containment) compares reference strings exactly — so `member:<UPPERCASE-UUID>` passed validation and then rendered as Unknown and matched no filter. `parseActorRef` now stores the canonical form. 3. The CLI, docs and skill all promised member email as a way to address a value, but `resolveAssignee` only matched id and name. Email is now an exact-match alias ranked with id matches (it is as unambiguous as an id), so the documented behaviour is real. This also makes `--assignee` accept an email, which is a strict improvement. 4. An active actor-property filter rendered as a bare count. The chips bar loads each directory lazily and gated the member directory on assignee/creator filters only, then looked names up in `config.options`, which actor properties do not have. It now detects actor-typed filters, loads the directory, and renders names plus an avatar stack. Found while fixing 4: the chips bar keyed its actor-name map by the membership row id (`m.id`) while every actor filter value carries `user_id`, so member names never resolved in the assignee and creator chips either. One token, same expression, fixed here rather than left next to the new code. Also drops two comments still describing the member/agent scope. Co-authored-by: multica-agent <[email protected]> * fix(properties): stop a single actor value overwriting an unreadable ref (MUL-6286) Round-two review finding. The previous fix only protected `multi_actor`: its toggle round-trips raw strings, so a reference kind this build cannot parse survives an edit. Single `actor` had no such protection. Once the backend widens actorPropertyKinds, an older client reading `agent:<id>` in a single actor property would filter it out, render the field as empty, and then — when the user "filled in the empty field" — replace it outright. The user sees themselves setting a blank field; what actually happens is a value they were never shown gets deleted. The editor now degrades to read-only when a single actor value holds a reference it cannot parse, and the display says so instead of showing the empty-state label. Clear is still offered, so a genuinely stale value can be removed deliberately. `multi_actor` stays editable — its toggle is already non-destructive. The read-only decision moved into an exported `isCustomPropertyReadOnly`, so all three reasons it can fire (archived, unknown type, unreadable actor ref) are covered by tests rather than being inline in the component. Also closes the two non-blocking gaps from the same review: - The "email outranks a name substring match" CLI test asserted a ranking its fixture could not actually exercise — no candidate's name contained the email. Added an agent named "Mailer for [email protected]", so the test now fails if email stops being ranked with id matches. - Extracted `buildChipActorNames` and asserted a member resolves by `user_id` and NOT by the membership row id — the pre-existing bug fixed in the last commit now has a test that would catch its return. Co-authored-by: multica-agent <[email protected]> * chore(migrations): renumber actor-types migration to 341 (MUL-6286) 332 was taken by 332_issue_status on main while this PR was open; the migration lint test caught the collision in CI. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |