Commit Graph
237 Commits
Author SHA1 Message Date
Jiayuan Zhangandmultica-agent 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]>
2026-08-23 17:15:46 +08:00
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]>
2026-08-23 16:43:58 +08:00
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]>
2026-08-23 13:47:06 +08:00
Martin MontesandMartin Montes 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]>
2026-08-23 12:56:06 +08:00
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]>
2026-08-21 14:33:04 +08:00
YYClaw 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
2026-08-21 14:26:48 +08:00
Sebastian San Blas 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]>
2026-08-21 13:21:07 +08:00
iiwish a3bd38da6a fix(cli): redact autopilot webhook credentials (#7365)
* fix(cli): redact autopilot webhook credentials

* fix(cli): preserve webhook token presence metadata
2026-08-21 12:39:43 +08:00
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]>
2026-08-20 17:33:29 +08:00
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]>
2026-08-20 16:09:32 +08:00
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]>
2026-08-20 16:03:51 +08:00
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]>
2026-08-20 15:22:15 +08:00
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]>
2026-08-20 14:32:12 +08:00
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]>
2026-08-20 14:17:18 +08:00
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]>
2026-08-20 12:54:46 +08:00
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]>
2026-08-20 12:54:19 +08:00
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 (1ea3cf1d7) outright.

squad_briefing.go's Squad Operating Protocol text is deliberately
untouched: it does not reference the retired modes, and its guest-leader
prohibition is a squad-protocol product decision, not workflow routing.

Guard tests now pin the fact anchor: conversational turns write nothing,
research on a todo issue leaves it todo, assigned deliverables land in
in_review (stage barriers and parent notifications key off that write),
and concurrent agents converge instead of flapping the board.

Co-authored-by: multica-agent <[email protected]>

* test(handler): align squad parent-status contract test with the fact-judgment brief (MUL-6417)

Co-authored-by: multica-agent <[email protected]>

* refactor(daemon): replace the prescriptive mention default with the facts behind it (MUL-6417)

"Default: NO mention" told agents what to do without why. The mention
syntax hides its semantics — it reads like a free social gesture but is
a spawn/notify operation — and every real incident was an agent acting
on a false need: "they should know" (comments already reach followers;
completion wakes are platform-owned) or courtesy (a thank-you run whose
only reply is another thank-you run). The section now states those facts
plus the cost asymmetry (a missed mention is one follow-up ask; a stray
one is a paid run), from which the old default is derivable.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): scope the mention notify-fact to followers; sync docs to the new status contract; compress (MUL-6417)

Review fixes from #7245 (Elon):

- "A mention routes work — it is never how someone finds out" was false
  for non-followers — a mention is exactly how a human who does not
  follow the issue finds out, and the absolute wording could suppress
  necessary escalation. The notify-fact is now scoped to followers, and
  a guard pin bans the overreach alongside the old prescription.
- Every surface that still taught the old status contract is synced to
  end-of-turn fact judgment: issues.mdx/zh (agent flips in_progress on
  start), assigning-issues.mdx/zh (todo → in_progress → in_review as the
  default arc), and working-on-issues-source-map.md:177 (reply turns
  gated on assignee — the exact gate this PR removes).

Also a compression pass over the PR-touched brief text (−474 bytes):
drop the intro signpost sentence (steps and status block are
self-contained), tighten the delivery routing and delivery bullet, and
compress the concurrency rationale to one clause. Ordinary brief is now
13,181 bytes vs 13,345 on main (−164), leader 14,286 vs 15,131 (−845);
with the per-turn marker deletion (−139/−142 per turn) every prompt
surface is a net reduction from the first turn.

Co-authored-by: multica-agent <[email protected]>

* docs(squads): retire the grant-routing ledger and status-first dispatch ordering (MUL-6417)

Review nit from #7245 (Elon): squad_briefing.go's design comment still
described the reply-mode grant routing and assignee-scoped arc this PR
removed. It now records the current mechanics — the workflow judges
status at end of turn, the leader's brief-side special case is dispatch
leaves the parent in_progress, and this section's job is the
owning/guest permission boundary at the Agent Identity layer. squads.mdx
(en/zh) stops describing a status-write-then-dispatch ordering: the
status point is now an end-of-dispatch-turn fact, consistent with the
assigning-issues docs.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-20 11:32:17 +08:00
Bohan Jiangandmultica-agent 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]>
2026-08-20 00:22:04 +08:00
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]>
2026-08-19 23:04:26 +08:00
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]>
2026-08-19 21:27:07 +08:00
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]>
2026-08-19 16:07:53 +08:00
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]>
2026-08-19 15:00:56 +08:00
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]>
2026-08-19 14:59:24 +08:00
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]>
2026-08-18 15:30:48 +08:00
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]>
2026-08-18 12:50:20 +08:00
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]>
2026-08-18 12:44:27 +08:00
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]>
2026-08-17 19:30:03 +08:00
GuillaumeandGuillaume Sayer 2014ae3613 feat(skills): bulk update from source (#6930)
Rescoped from the original freshness-awareness PR per maintainer review on
#6930: the upstream freshness tracking (import-time version markers, the
refresh-check endpoint and cache, and the detail-page up-to-date indicator)
is dropped entirely; only the bulk update remains, simplified.

The skills list batch toolbar gains an Update action: the confirm dialog
shows how many selected skills will be re-downloaded from their hosted
source and how many are skipped (manual/runtime/no permission), then reuses
the existing single-skill POST /api/skills/:id/refresh sequentially across
the updatable selection. No freshness probe first — re-fetching an
already-current skill costs one download on an explicit click.

Kept from the original design: per-item failure tolerance (one 409 must not
strand the batch), a partial-failure toast, and the selection surviving a
partial run so retry is one click. Frontend-only; no API or schema changes.

Co-authored-by: Guillaume Sayer <[email protected]>
2026-08-17 18:46:26 +08:00
d0bd6683f1 MUL-5733: keep Go security patches current (#6409)
* fix(release): keep Go security patches current

Co-authored-by: multica-agent <[email protected]>

* fix(release): address vulnerability review

Co-authored-by: multica-agent <[email protected]>

* fix(release): restrict vulnerability bypass

Co-authored-by: multica-agent <[email protected]>

* fix(release): require Go 1.26.6

Co-authored-by: multica-agent <[email protected]>

* docs: align Go prerequisite with security floor

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Sol-Boy <[email protected]>
2026-08-17 16:18:53 +08:00
3b08423684 MUL-6260 fix(daemon): treat custom runtime fixed_args as the argv prefix (#7052)
* fix(daemon): treat custom runtime fixed_args as the argv prefix (MUL-6260)

A custom runtime profile configured as `ccms start q36` failed every task
with `unknown option '-p'`. The profile's fixed_args were merged into
ExecOptions.ExtraArgs, which each backend appends *after* the protocol
flags it hardcodes, so the wrapper saw `-p` before its own `start`
subcommand had been selected and rejected it (GH #7046).

Ordering was only half the defect. ExtraArgs is opt-in — six of the
twenty-one backends read it — so on the other fifteen families, including
every ACP one, fixed_args were silently discarded rather than misordered.
Model discovery and `--version` probing carried the profile's path but not
its arguments, so even a fixed launch would have left the model picker and
the reported version describing the wrapper instead of the CLI behind it.

fixed_args now travel as agent.Config.LaunchPrefix and are spliced in
directly after the executable, ahead of everything a backend builds. The
prefix is applied by a new Command boundary in launch.go through which
every runtime process in the package is constructed — launches, version
probes and discovery alike — so a backend cannot forget it. A guard test
fails the build if any file outside launch.go calls os/exec directly;
distributed opt-in is what let ExtraArgs rot, and repeating it would rot
the same way.

Flag-style commands (`agent --model composer-2.5`) are unaffected: a CLI
takes its own flags in any position, which is why prefix-first can be the
single order rather than a per-runtime setting.

One deliberate behaviour change. The prefix used to sit last and win every
last-wins parse, so a profile pinning `--model` silently overrode the model
chosen on the agent — the UI showed one model and ran another. Prefix-first
inverts that: the more specific per-agent choice is the later token and
wins. Overlaps are logged. Protocol-critical flags are filtered out of the
prefix once in New(), which knows the family; positional tokens always pass
through. Codex additionally strips `-c` overrides aimed at daemon-managed
config namespaces, since those beat config.toml from any argv position.

The order was never documented, and every shipped fixed_args example is
flag-style, so this fills in a contract rather than breaking one.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): close the launch-prefix gaps found in review (MUL-6260)

Four defects from the first round of review on the fixed_args-as-argv-prefix
change.

Hermes could escape its own task overlay. The profile selection was resolved
from custom_args alone and stripped from custom_args alone, so a profile
configured as `hermes -p research` kept that flag in the launch prefix and
re-pointed HERMES_HOME past the overlay the daemon had just built. Resolution
now parses the argv hermes actually sees — launch prefix then custom args, in
process order — and the strip covers both regions. It also removes every
selection rather than the first: taking out the first promotes the second, and
two separately-configured argv regions make "two selections" an ordinary
configuration rather than a user mistake.

Positional tokens were not passing through as documented. filterLaunchPrefix
delegated to filterCustomArgs, whose blocklists deliberately carry positional
tokens — hermes and qwenpaw block `acp`, traecli blocks `acp`/`serve`, grok
blocks `agent`/`stdio`/`serve` — because in custom_args those are someone
re-issuing the backend's own subcommand. In a launch prefix the same token is
the command's identity, so `hermes-wrapper acp tenant` was being rewritten to
`tenant`. Only a leading `-` now makes a token eligible for the blocklist; a
blocked flag still consumes its separate value so no stray value survives to be
read as a subcommand.

Codex fast mode lost to the prefix. `--disable fast_mode` beats `--enable
fast_mode` regardless of argv order and `-c features.fast_mode=false` beats
config.toml from any position, so prefix-first does not protect an explicitly
selected priority tier. The removal half of enforceCodexFastMode is now a
separate helper applied to the prefix too; the managed enable is still appended
once, on the final args.

Windows CI could not compile the daemon tests: the Command migration left an
unused `path := runtimeCmd.Path` in a hook whose body never referenced it.
Verified with GOOS=windows and GOOS=linux vet, which is what should have caught
it the first time.

Also softens the docs and the Command doc comment: flag-style prefixes usually
parse the same in either position, but a CLI that separates global from
subcommand flags can care, so the runtime docs now name the position change and
tell operators with existing fixed_args to re-run one task after upgrading
instead of promising the move is invisible.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): resolve and strip the Hermes profile from the real launch argv (MUL-6260)

Second-round review found the Hermes profile chain still reasoning about a
hand-assembled approximation of the command line instead of the command line
itself. The backend launches `<prefix> acp <filtered custom args>`, but the
daemon assembled `<prefix> <custom args>` — no `acp`, no filtering — and that
token participates in Hermes' own argv scan.

With fixed_args `--model` and custom_args `-p research`, the approximation
reads `-p` as `--model`'s value and finds no selection, so the overlay is
seeded from the default home; the real `--model acp -p research` skips
`--model acp` and selects `research`. The selector was then stripped, so the
task stayed inside the overlay — but an overlay built from the wrong profile,
silently.

Stripping had the matching defect, and this one was still an escape rather than
a mismatch. A launch prefix ending in a bare `-p` captures the backend's `acp`
as its profile value, so neither region holds a complete selection and
per-region stripping removes nothing: `-p acp research --yolo` reaches Hermes
intact and re-points HERMES_HOME straight out of the overlay.

Both halves now go through one assembly. agent.HermesLaunchArgv builds exactly
what hermesBackend.Execute builds — the backend calls the same helper, so they
cannot drift — and the daemon resolves from it. StripHermesProfileSelectors
strips against that same argv and hands surviving tokens back to the region
they came from, iterating to a fixed point because removing one selection
promotes the next. The `acp` token is never removed; the backend re-adds it,
and dropping the flag that captured it is what breaks the selection.

StripAllHermesProfileArgs, hermesLaunchArgs and hermesEffectiveArgs are gone:
each was only ever correct on a complete argv, and keeping them would leave the
same footgun for the next caller.

Tests cover the case the review named (prefix ending in a value-taking flag,
custom args opening with a selector), the straddling-selection escape, multiple
occurrences across both regions, and an invalid value left untouched because
Hermes discards it anyway.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-17 15:11:36 +08:00
0a1357498a MUL-6247: rate limit workspace invitations (#7030)
* feat(workspaces): rate limit invitations (MUL-6247)

Co-authored-by: multica-agent <[email protected]>

* fix(workspaces): preserve invitation rate budgets

Co-authored-by: multica-agent <[email protected]>

* fix(workspaces): avoid partial limiter failures

Co-authored-by: multica-agent <[email protected]>

* fix(workspaces): relax recipient invitation limit

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-17 12:51:39 +08:00
9f85d72a06 MUL-6193 Support Slack message attachments in chat (#6975)
* feat(slack): ingest inbound message file attachments as chat media

Slack messages carrying files now come through to the chat: the inbound
translation captures each file's id, name, mimetype, size, and private
download URL into the raw envelope, and a new engine.MediaResolver fetches
the bytes with the installation's bot token, stores them through the shared
intent-ledger pipeline, and binds them as chat attachments. The download
client only talks to Slack-owned hosts so the bot token cannot leak, and an
HTML login-page response (missing files:read scope) is rejected instead of
stored as the file.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(slack): address attachment review findings

* docs(slack): make media comments standalone

* fix(slack): harden inbound attachment ingestion

Address second-round review findings on the Slack media resolver.

- Restore the bot token on permitted redirects. net/http forwards
  Authorization only to the same host or a subdomain, so a hop to a
  sibling such as files-origin.slack.com arrived unauthenticated, Slack
  answered with its HTML login page, and the file was silently dropped.
  The redirect policy has already proved the destination is Slack-owned.
- Drop files the resolver could never fetch at translation time. Remote
  "external" files point url_private at Google Drive or Dropbox, so
  carrying them made HasMedia promise media the pipeline cannot bind:
  the message got a deferred run and an intent-ledger row per doomed
  download.
- Share the Router's media deadline across the files still to fetch.
  Ten files at a flat 30s each ran far past the 45s budget, and the
  Router discards every ref once it expires, so one slow file cost the
  whole message its attachments.
- Refuse an oversized file on its declared size, before it costs an
  intent row and a full transfer. Correct the limit wording to MiB.
- Cover the redirect policy, the size boundary, the spent budget, and
  the off-Slack filter with regression tests, and drive the file_share
  translation test through slackevents.ParseEvent instead of a
  hand-built MessageEvent.Message.
- Document sending files, the per-file and per-message limits, and the
  skip-silently behaviour in all four locales.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-17 00:08:53 +08:00
4d495056e8 [MUL-6139] Expand Plugin skills and hosted MCP connections (#6920)
* feat(plugins): add remote MCP runtime

Co-authored-by: multica-agent <[email protected]>

* chore(plugins): add private Exa example

Co-authored-by: multica-agent <[email protected]>

* docs(plugins): remove private plugin guide

Co-authored-by: multica-agent <[email protected]>

* fix(plugins): stabilize remote MCP tool digests

Co-authored-by: multica-agent <[email protected]>

* fix(plugins): preserve remote MCP notification status

Co-authored-by: multica-agent <[email protected]>

* fix(plugins): make Exa example runnable end to end

Co-authored-by: multica-agent <[email protected]>

* fix(codex): record MCP tool calls in agent log

Co-authored-by: multica-agent <[email protected]>

* chore(plugins): add community skills example

Co-authored-by: multica-agent <[email protected]>

* feat(plugins): support multi-file skill bundles

Co-authored-by: multica-agent <[email protected]>

* feat(plugins): add Mobbin MCP example

Co-authored-by: multica-agent <[email protected]>

* chore(plugins): bundle full Matt Pocock skill set

Co-authored-by: multica-agent <[email protected]>

* feat(plugins): add remote MCP OAuth connections

Co-authored-by: multica-agent <[email protected]>

* fix(plugins): gate enable on remote MCP readiness

Co-authored-by: multica-agent <[email protected]>

* fix(plugin): authenticate remote mcp credential broker

Co-authored-by: multica-agent <[email protected]>

* fix(plugins): harden remote mcp oauth flow

Co-authored-by: multica-agent <[email protected]>

* refactor(plugins): use a single feature flag

Co-authored-by: multica-agent <[email protected]>

* fix(plugins): clean up remote MCP OAuth state

Co-authored-by: multica-agent <[email protected]>

* chore(plugins): remove Matt Pocock example

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-16 04:54:32 +08:00
2c0912b6ec MUL-5421: make workspace MCP servers an explicit per-agent assignment (#6976)
* feat: make workspace MCP servers an explicit per-agent assignment

#6961 shipped workspace MCP as a shared document every agent inherited
automatically. That is the wrong shape: a workspace MCP server should behave
like a workspace skill — a library entry that reaches an agent only when
someone gives it to that agent, with a per-agent on/off toggle. Creating one
now gives it to nobody, so nothing propagates implicitly.

Model (mirrors `skill` + `agent_skill`):

  - `workspace_mcp_server` — the library, keyed by id so renaming is safe
  - `agent_mcp_server` — the assignment, carrying its own `enabled` toggle
  - the agent's own `mcp_config` stays what it was: servers private to it

At claim time the effective set is runtime servers < (assigned AND enabled
workspace servers + the agent's own), with the agent's own entry winning a name
collision. 318 drops `workspace.mcp_config`: leaving it would keep a second,
silently inherited source alive. No data migration — converting each workspace
document into library entries would ASSIGN them to every agent, which is the
behaviour being removed, and the column shipped in the same unreleased cycle.

The write-only secret contract, the legacy-container normalization, and the
transport guards from the previous round are all preserved; the resolver is
re-pointed at the assignment list rather than a workspace document.

UI: workspace Settings → MCP is now a library (adding says so, and renaming is
allowed because assignments key off the id); the agent's MCP tab replaces the
auto "Inherited from workspace" list with the servers actually assigned to it,
a picker to add more from the library, and a per-assignment toggle.

Co-authored-by: multica-agent <[email protected]>

* fix: fence the MCP writes, stop laundering unknown transports, gate on canEdit

Three findings from review:

1. Neither new table carries a foreign key (repo rule), so nothing in the
   database catches a write landing after the row it depends on is gone. Create
   and assign now run in a transaction that joins the workspace teardown fence
   (#5219), and assign additionally holds the target server FOR SHARE while
   DeleteWorkspaceMcpServer holds it FOR UPDATE — so an assignment either lands
   before the delete and is swept, or waits and finds the server gone. Driven
   by two two-transaction race tests, verified to fail without the locks.

2. `mcpTransportOf` classified `{"type":"websocket","url":"wss://…"}` as http,
   because an unrecognised type fell through to the url check. That told the
   settings UI the guided form could edit the entry, and saving from it rewrites
   the protocol to `type: "http"` — the failure the client-side guard was added
   to prevent, reintroduced from the server. An explicit type is now returned
   verbatim; inference from command / url happens only when none was declared,
   which is lossless.

3. The agent MCP tab showed the picker, toggles, and remove button to members
   who cannot edit the agent, where every click 403s. It now takes `canEdit`
   like SkillsTab and hides the write affordances, keeping the inventory
   readable.

318 keeps dropping `workspace.mcp_config` with no data migration and no
expand-contract window, now recorded as an explicit maintainer decision (Bohan,
2026-08-14): 314 merged in this same cycle and was never deployed, so there is
no stored document to preserve and no old binary reading the column. The
comment spells out what this file would have to become if that ever stops
holding.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-14 19:49:37 +08:00
Bohan Jiang d96c936430 docs: link the Multica CLI skill from the README and CLI docs (#6978)
The skill that teaches Codex, Claude Code, and Cursor to operate Multica
through the authenticated CLI lives at multica-ai/multica-cli, but nothing
in this repository points at it — a search for the repo name returns only
release-asset filenames. Users have filed feature requests for a skill that
already exists (#6968, #4681), which is a discoverability problem, not a
missing feature.

Adds a row to the README documentation table and a section to the CLI docs
page in all four languages, plus a Next steps entry.

Docs-only change; no build or tests were run.
2026-08-14 19:47:23 +08:00
736838af86 MUL-5421: workspace-level MCP configuration (resolution contract, write-only API, Settings UI, CLI) (#6961)
* feat(server): resolve a workspace-level MCP config under each agent's

Adds workspace.mcp_config and layers it beneath the agent's own config on
every claim, so a workspace's shared MCP servers no longer have to be
re-entered on each agent (GH #6062).

Resolution contract:
  - agent config absent (NULL/null) -> inherit the workspace document
  - agent declares >=1 server       -> union by server name, agent wins
  - agent declares no server ({})   -> opt out entirely

The last rule is what keeps shared credentials from silently reaching an
agent that was deliberately given none: "managed but empty" already means
"this agent runs with zero MCP" under the three-state contract from #3545,
and it keeps meaning exactly that.

The merged document keeps the AGENT's top-level keys, so a legacy `mcp`
container survives untouched, and a shared server is skipped when the agent
already declares that name under either spelling. On malformed input the
agent's config is returned unchanged: a bad shared document must never take
away servers an agent runs with today.

Read on every claim like agent.mcp_config, so an admin's edit lands on each
agent's next task with nothing to restart.

Co-authored-by: multica-agent <[email protected]>

* feat(server): add the workspace MCP configuration API

GET /api/workspaces/{id}/mcp-config is member-visible so an agent's settings
view can tell what it inherits, but the document itself follows the same
visibility rules mcp_config already has on agents: agent actors never read it,
the workspace always-redact setting wins over any role, and otherwise only the
owner/admin who may write it can read it back. The coarse server_count stays
visible either way — it carries no credential material.

PUT is owner/admin and replaces the whole document (so removing a shared
server is expressible), with an explicit null to clear the layer. It is
shape-validated at the edge, unlike the agent column: one bad document here
would otherwise fail-soft on every agent's claim for the whole workspace.
Agent actors are refused even when their PAT satisfies the role gate.

Co-authored-by: multica-agent <[email protected]>

* feat(cli): add `multica workspace mcp get|set`

Reuses resolveMcpConfig so the workspace surface agrees with `agent update`
on what a valid document is, on the three mutually exclusive input channels,
and on 'null' meaning clear. Table output prints server names and transports
only — never entry bodies — so a summary stays safe to paste into a ticket;
the JSON default is how an admin reads the document itself.

Documents the new inheritance in the agent-creation built-in skill (an agent
with a private server no longer has to re-list the shared ones, and `{}` is
now the only way to give an agent no MCP at all) and adds the command to the
CLI reference.

Co-authored-by: multica-agent <[email protected]>

* fix(server): keep a legacy agent's own MCP servers when the workspace shares some

The resolver wrote shared servers into `mcpServers` and left an agent's legacy
top-level `mcp` entries beside them. The daemon's runtime merge only falls back
to `mcp` when `mcpServers` is ABSENT, so as soon as a workspace shared anything,
an OpenCode agent that stored its servers under the legacy container ran with
the shared set and silently lost every private server of its own.

Fold both containers into the canonical `mcpServers` when merging, and drop the
legacy key so the daemon can never read a second, stale copy. Nothing changes
for an agent whose workspace shares no servers: that path still passes the
agent document through byte-for-byte.

The regression guard runs the real chain — ResolveWorkspaceMcpConfig into the
daemon's mergeRuntimeAndAgentMcpConfig — because the two halves only agree if
the resolver emits the container the merge reads, which is exactly what broke.
Verified it fails against the previous resolver.

Co-authored-by: multica-agent <[email protected]>

* feat(server): make the shared MCP configuration write-only

Upstream #6062 asks for shared secrets to be write-only and never echoed back;
the previous read path handed the whole document to owner/admin, which also tied
"audit what is shared" to "read the credentials".

GET now returns only a non-secret inventory — name, transport, enabled — for
every role. Not a redaction: there is no document field and no `_redacted` flag,
because nothing secret is in the payload to gate. A url counts as secret here; a
Composio-style session url is a bearer credential on its own.

That removes the client's ability to read-modify-write, so editing one server
gets its own endpoints: PUT/DELETE /mcp-config/servers/{name} merge server-side
under a row lock (two admins editing different servers can no longer lose one
another's write). Deleting the last server clears the column rather than storing
an empty document, so agents fall back to their own config instead of inheriting
a managed-empty one. The whole-document PUT stays for the declarative CLI path.

Co-authored-by: multica-agent <[email protected]>

* feat(cli): add `workspace mcp add|remove`, and stop printing the document

`workspace mcp get` now renders the server inventory in both output formats —
there is no document to print, and the previous JSON default would have written
whatever the endpoint returned straight to a terminal.

Since the document can no longer be read back, `set` (full replace) is not
enough to change one server: `add <name>` and `remove <name>` map to the
per-server endpoints. `add` takes the entry through the same three mutually
exclusive secret-safe channels as `--mcp-config`, generalized here so both
surfaces agree on what a valid payload is; `null` is rejected for a single
entry, where it is far likelier to be a mistake than an intent to clear.

Adds the CLI test coverage this surface was missing: endpoint and body per
command, `null` clear, mutual exclusion, name escaping in the path, and a guard
that a regressed server returning the document cannot get it printed.

Co-authored-by: multica-agent <[email protected]>

* docs: correct the workspace MCP contract in the built-in skill and CLI reference

- "`{}` is the only way to opt out" was wrong: any config declaring no servers
  opts out, which is what the implementation and tests do.
- The source map pointed the API row at workspace_mcp.go; the handlers live in
  workspace_mcp_api.go.
- Document the write-only read path, the legacy-container normalization, and the
  new per-server commands.

Co-authored-by: multica-agent <[email protected]>

* feat(web): manage workspace MCP servers from Settings

The user journey for this feature is a settings screen, not the CLI. Adds a
workspace Settings → MCP tab that lists the shared servers and adds, replaces,
and removes them one at a time through the per-server endpoints, reusing the
agent tab's McpServerDialog for the form/JSON editor.

Because the stored configuration is write-only, an edit opens an empty form and
REPLACES the entry rather than prefilling it. The screen says so instead of
implying the blank form is the saved state. Members see the inventory (nothing
in it is secret) but no write affordances.

The agent's MCP tab gains the matching "Inherited from workspace" section, with
an Overridden badge on a shared server the agent shadows by name, and an
explicit note when the agent's managed-but-empty config opts it out of the
workspace layer entirely — otherwise the effective server set is impossible to
reason about from the agent's own screen.

Co-authored-by: multica-agent <[email protected]>

* fix(client): stop the write-only boundary leaking through the clients

The server never returns the stored document, but neither client enforced that
on its own side, so a regressed (or hostile) backend could still push secrets
into the frontend and onto a terminal:

- the two workspace-MCP zod schemas were `.loose()`, so unknown fields — a
  top-level `mcp_config`, or `url` / `headers` / `env` inside a server entry —
  survived parsing into the app object and the query cache. These are now the
  only schemas in that file that strip: forward-compatibility is the right
  default everywhere else, but here keeping unknown keys is the hole.
- the CLI decoded into `map[string]any` and re-encoded it, so the DEFAULT JSON
  output printed whatever arrived. It now decodes into a named struct, which is
  what makes leaking impossible rather than merely unlikely.

The old "never prints secrets" test forced `--output table` and so missed the
default path entirely. Replaced with a poisoned-response fixture (secrets at the
top level AND inside a server entry) asserted across both output formats and on
the write path; verified it fails against the previous renderer.

Co-authored-by: multica-agent <[email protected]>

* fix(web): stop a workspace MCP edit from silently becoming an add

The shared dialog lets the name be edited and this API has no atomic rename, so
renaming `linear` to `linear-v2` upserted a second server, left the original in
place, and still toasted "updated". Editing now pins the name (the dialog gained
a `lockName` prop) and the upsert targets the entry that was opened.

Also seed the edit form's transport from the safe summary: the entry cannot be
read back, and `formFromConfig({})` defaults to HTTP, so editing a known stdio
server opened the wrong form.

Co-authored-by: multica-agent <[email protected]>

* fix(web): show the agent's real effective MCP set

Two ways the agent tab was stating things it did not know:

- With `mcp_config_redacted`, the viewer cannot tell an explicit empty document
  (opted out) from one that overrides some names — but the tab listed every
  workspace server as inherited anyway. It now says inheritance cannot be
  determined instead of guessing.
- The runtime list marked a server "overridden" using the agent's names only.
  The real precedence is runtime < workspace < agent, so a shared server hides a
  same-named runtime one too; that was under-reported. Overrides are now computed
  from workspace + agent names, and from the agent's alone when it opted out.

Co-authored-by: multica-agent <[email protected]>

* docs: correct the opt-out claim and apply the zh terminology conventions

An agent whose config declares no servers opts out of the WORKSPACE layer; it
does not run with zero MCP. The daemon still merges the runtime's own local
servers under whatever the claim carries — which the daemon test on this branch
already asserts, so only the prose was wrong (skill doc, CLI help, UI copy).

The new zh-Hans strings also used `agent` / `url` / `header` raw; per
conventions.zh.mdx those are 智能体 / URL / 请求头.

Co-authored-by: multica-agent <[email protected]>

* fix(i18n): restore the zh-Hans quick-actions strings this branch clobbered

My earlier terminology pass ran a `re.sub` for `description` /
`delete_description` with count=1, which matched the FIRST such keys in the
file — `quick_actions`, not `mcp` — so Quick Actions ended up describing shared
MCP servers and its delete confirmation talked about sharing. A user-visible
regression in an unrelated feature, and one no key/type check can catch because
both values stayed valid strings.

Restored both to their origin/main text. Audited all eight locale files the
branch touches the same way; these two were the only unintended changes.

Co-authored-by: multica-agent <[email protected]>

* fix(web): never edit a non-stdio/http MCP server through the rewriting form

The guided form only expresses stdio and http, and saving from it REWRITES the
entry — `configFromForm` emits `type: "http"` for anything non-stdio. The server
classifies and returns `sse`, so editing an SSE server opened the Streamable
HTTP form and saving silently changed its protocol, which can break the server
outright.

Entries the form cannot express now take the JSON path, which round-trips what
is typed, and the Form tab is disabled rather than merely unselected — switching
to it would rewrite them just the same. The rule keys off the saved config when
there is one and off the safe summary's transport when there is not, so it
covers both the workspace tab (write-only, summary only) and the agent tab,
where the same rewrite was already reachable for a saved SSE entry.

Also fixes the garbled `multica workspace mcp get|list-only` in the built-in
skill, which reads as a shell pipe.

Co-authored-by: multica-agent <[email protected]>

* fix(web): judge form-editability by the entry's explicit type, not its display bucket

The previous guard asked `mcpTransport(config)` whether the form could express a
saved entry, but that is a lossy DISPLAY classifier: it buckets anything with a
`url` as http regardless of `type`. So `{"type":"websocket","url":"wss://…"}`
still looked form-editable and `configFromForm` rewrote it to `type: "http"` on
save — the very rewrite the guard exists to prevent. SSE happened to be caught
only because the classifier has a dedicated branch for it.

Saved entries are now judged on the explicit `type`, allowlisting exactly what
`configFromForm` writes back unchanged (local / stdio / remote / http /
streamable-http), with the no-type case still inferred from command / url. The
summary path keeps using the transport string, which the server emits from a
closed set.

Tests cover both unknown-type spellings on the agent side and pin the common
shapes to the form — this is a guard against rewriting entries, not a general
retreat to the JSON editor. Verified the websocket case fails against the
previous check while sse passed, which is why the earlier test missed it.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-14 18:28:26 +08:00
Danila Katalshov 3025c38027 MUL-6019: fix(codex): add MULTICA_CODEX_FIRST_TURN_TIMEOUT to raise the first-turn ceiling (#6753)
* fix(codex): add MULTICA_CODEX_FIRST_TURN_TIMEOUT to raise the first-turn ceiling

The first-turn no-progress watchdog caps at the 60s default that the
configured semantic-inactivity timeout can only shrink, never raise. An
app-server that is legitimately slow to its first event (heavy MCP boot,
cold model catalog) is killed at 60s with no way to extend it.

Add a dedicated MULTICA_CODEX_FIRST_TURN_TIMEOUT knob (default 0 = unset)
threaded through ExecOptions to codexFirstTurnNoProgressTimeout, which now
honors an explicit value upward and is otherwise byte-identical. Default
behaviour and the clamp contract are preserved.

Supersedes #5959. Refs #3262.

* fix(codex): pin and document the first-turn/semantic timer interaction

The MULTICA_CODEX_FIRST_TURN_TIMEOUT override only raises the first-turn
watchdog, not the semantic-inactivity timer that the same first status:running
arms concurrently. An override above the semantic timeout is therefore
truncated to it, and the resulting failure is reclassified as semantic
inactivity — which also skips the #3291 model-catalog startup retry. The prior
docs, comment, and test wording implied the override "wins outright / unbounded
above", hiding this real upper bound and the configuration-dependent loss of
automatic recovery.

- Warn at config load when MULTICA_CODEX_FIRST_TURN_TIMEOUT exceeds the
  semantic timeout, naming both the truncation and the disabled startup retry.
- State the effective bound (semantic timeout + overall execution timeout) in
  all four environment-variables*.mdx pages, and that both must be raised
  together when the desired first-turn wait exceeds the semantic limit.
- Narrow the "wins outright / unbounded above" resolver comment and test name
  to describe only the first-turn watchdog ceiling.
- Add a runtime regression test that drives the real run loop with an override
  above the semantic timeout and asserts the semantic timer wins the race and
  the startup retry is skipped; add a LoadConfig empty/explicit/zero test
  (this knob is environment-only, no Overrides parity).

Addresses review on #6753. Refs #3262 / #5959 / #3291.

* fix(codex): warn at equal first-turn/semantic timeouts, require strictly greater

multica-eve's re-review on #6753: the warning fired only when the first-turn
timeout was strictly greater than the semantic timeout, and told operators to
raise the semantic timeout "to match". But the semantic timer is armed first
(codex.go resets it before creating the first-turn timer), so at equal durations
the semantic deadline is reached first and Go's select does not deterministically
favour the first-turn branch — the failure can still be classified as semantic
inactivity and skip the #3291 startup retry. An operator following the old
guidance to equal values got no warning and silently lost the retry.

- Warn when first-turn >= semantic (was >), and tell operators to set the
  semantic timeout strictly above the first-turn timeout (with margin), not
  "to match".
- Update all four environment-variables*.mdx locales accordingly.
- Add TestLoadConfig_CodexFirstTurnTimeoutEqualToSemanticWarns pinning the
  equal-duration warning and the strictly-greater no-warning boundary.

Addresses review on #6753. Refs #3262 / #3291.
2026-08-14 15:30:42 +08:00
b5a48eca13 fix(agent tasks): prevent duplicate self-assignment work (MUL-6168) (#6956)
* fix(agent tasks): avoid duplicate self-assignment runs

Co-authored-by: multica-agent <[email protected]>

* fix(agent tasks): address duplicate-run review

Co-authored-by: multica-agent <[email protected]>

* fix(agent tasks): resolve sibling-run nits

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-14 13:57:09 +08:00
AriesL e3ec3f8b57 MUL-6162 docs: sync landing copy and docs for the DeepSeek Harness runtime (#6940)
Completes the user-facing surface for the DeepSeek Harness runtime merged in #6923: landing copy (21 -> 22 tools), README, CLI_AND_DAEMON, SELF_HOSTING, provider comparison and install docs across en/zh/ja/ko, plus the provider logo.

Review follow-ups folded in: the mark is inlined so currentColor tracks the app theme (an <img>-loaded SVG pinned it to black in dark mode), the landing enumerations and environment-variable docs are complete in all four locales, and the new rows sit in their tables' existing order.
2026-08-14 02:51:56 +08:00
iiwish 698013cedb MUL-5771 feat(daemon): allow configuring workspaces root (#6830)
* feat(daemon): allow configuring workspaces root

* fix(daemon): align disk usage workspace roots
2026-08-14 01:58:29 +08:00
09efbe597d docs: explain the two local-directory execution modes (MUL-5707) (#6913)
The mode reference existed but three things had drifted from the product:

- The docs only ever named `in_place` / `worktree`, while Desktop labels the
  same choice Direct / Parallel. A user reading either surface could not tell
  they were the same decision. Both names now appear on the mode headings and
  in the section intro.
- The mode is now chosen while creating the project, and Desktop preselects
  Parallel for a git repository the runtime can isolate. The add-a-directory
  steps still described the old add-then-edit path only, so the preselection
  rule — and the fact that existing resources keep their saved mode — is
  written down where someone deciding will actually read it.
- The gate was described as a daemon version floor. #6904 made it a declared
  capability, precisely because a dev build carries a version string that
  looks new enough while having none of the implementation. The prose said
  "too old" / "below the minimum" in all four locales.

Also points `waiting_local_directory` in troubleshooting at Parallel mode —
someone landing there because a task is queued should know the queue is a
property of Direct mode, not of local directories — and documents
`--execution-mode` in the CLI reference table.

All four locales updated. MDX renders and every cross-page anchor resolves
(checked against the running docs site).

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-13 17:55:36 +08:00
6db6b235ba MUL-6126: fix(runtime): make private runtimes owner-only in the API and CLI too (#6905)
* MUL-6126: fix(runtime): make private runtimes owner-only in the API and CLI too

The web UI has always disabled another member's private runtime in every
runtime picker, but canUseRuntimeForAgent let a workspace owner/admin bind
an agent to it anyway, so the same action succeeded over the API and CLI and
failed in the UI (#6898).

Unify on the stricter rule the clients already show: a private runtime is
usable only by its owner, a public one by anyone in the workspace, and
workspace role plays no part. A private runtime is someone's own machine —
running an agent there spends their credentials and their files, which is
not an administrative decision. Admins keep canEditRuntime, so opening a
runtime up stays possible by flipping it to public, an explicit and visible
act instead of a silent bypass.

Drops the role branch from canUseRuntimeForAgent, which every bind path
already shares (create, update, builder create/switch, Mika, onboarding),
and retires the "or a workspace admin" half of the 403s and the matching UI
copy and docs in all four locales. On the client the inspector runtime
picker now calls the shared isRuntimeUsableForUser instead of re-deriving
the rule inline, so there is one predicate per side and they agree.

Existing agents already bound to another member's private runtime keep
running; only new binds and re-binds are refused. An ownerless private
runtime is now refused up front rather than at task claim, where it already
failed for want of an owner to mint the agent token (MUL-3292).

Co-authored-by: multica-agent <[email protected]>

* MUL-6126: test(server): own the integration fixture runtime

The router integration fixture registered its runtime with no owner_id, so
it relied on the workspace-owner override that this branch removes: every
agent these tests create through POST /api/agents now 403s on a private,
ownerless runtime.

Give the fixture runtime to the fixture user, matching the handler test
fixture and every runtime a real daemon registers with a member credential.
The gate is what changed; ownerless was never the shape under test.

Co-authored-by: multica-agent <[email protected]>

* MUL-6126: fix(runtime): let only the owner share a runtime, and refuse ownerless binds

Two holes in the owner-only rule this branch introduced, both found in review.

canEditRuntime still let a workspace admin flip someone else's runtime to
public, which is the old override with one extra request in front of it: the
admin publishes the member's machine to the whole workspace, binds, and can
flip it back, with no owner consent anywhere in the loop. Visibility now has
its own owner-only gate (canSetRuntimeVisibility) while canEditRuntime keeps
covering rename and delete, which hand out nobody's credentials. A no-op
resubmit of the unchanged value is still tolerated so an admin renaming a
runtime through a PATCH-as-PUT client doesn't start 403ing. The runtime detail
page follows: the Private/Public editor is the owner's, everyone else gets the
read-only chip, whose tooltip now speaks in the third person because admins
read it too.

The bind gate also let an ownerless runtime through whenever it was public.
Nothing can run there — task claim needs an owner to mint the agent's task
token and cancels the task without one (MUL-3292) — so binding produced agents
that failed at their first task. Both predicates now check for an owner first,
server and client alike.

Docs, UI copy and the RuntimeVisibility type comment drop the last claims that
an admin may bind or share a private runtime.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-13 17:55:09 +08:00
31645cc510 [MUL-6125] Ship Private Skill Plugin developer loop (#6900)
* [MUL-6125] Ship private Skill Plugin developer loop

Co-authored-by: multica-agent <[email protected]>

* [MUL-6125] Address Private Plugin review findings

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-13 17:49:43 +08:00
c9727e6b2b feat(daemon): worktree mode for local_directory resources (MUL-5707) (#6759)
* feat(daemon): worktree mode for local_directory resources (MUL-5707)

Local directory resources serialise every task on a per-path mutex, so two
tasks on one folder queue behind each other. This adds an opt-in worktree
mode: each task gets its own git worktree of the user's repo inside the
daemon's env root, so tasks run concurrently and deliver their work as a
branch instead of editing the user's working copy.

Opt in per resource with execution_mode=worktree (default stays in_place,
so existing resources are unchanged). Requires a git repo with at least one
commit; a non-git directory fails with an actionable message rather than
silently falling back to serial execution.

Design points that make the mode safe to use:

- The agent starts from what the user sees, not HEAD. Tracked edits are
  captured with `git stash create` (which writes a commit object without
  touching the index, working tree, or stash list) and replayed into the
  worktree; untracked non-ignored files are copied, bounded at 2000 files /
  200MB with the skipped count logged at warn level.
- That replayed state is committed as a baseline, so "did this task change
  anything?" has an exact answer and `git diff <baseline>..<branch>` is
  precisely the agent's work.
- Nothing is silently discarded: leftovers are committed to the branch
  before the worktree is removed, including when the task fails partway.
- A task that changes nothing leaves no branch behind.
- Sidecars stay out of the branch by running the existing
  CleanupRuntimeConfig + CleanupSidecars pass over the worktree before
  finalizing. The .git/info/exclude approach repocache uses is not usable
  here: a linked worktree resolves info/exclude to the user's own common git
  dir, so it would change what `git status` hides in their checkout.
- Worktree-mode env roots do not get the local_directory GC exemption, so
  they age out normally instead of accumulating one env root per task.
- `git worktree prune` on create self-heals registrations orphaned by a
  crashed daemon.

Adds --execution-mode to `project resource add/update`, and updates the
docs and the projects-and-resources built-in skill.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): close data-loss and isolation gaps in worktree mode (MUL-5707)

Review follow-up on #6759. Six issues, all on failure or skew paths.

1. Preserve work when the final commit fails. Finalize logged a warning and
   then ran `git worktree remove --force`, destroying the agent's changes.
   commit.gpgSign with no key available to the daemon triggers this, and
   --no-verify does not disable signing. Finalize now returns an error and
   deliberately keeps the worktree — it stays registered, so `git worktree
   list` points at the work — and the daemon fails the task instead of
   reporting success with a branch that is missing the changes.

2. Fail closed on an unsupported execution_mode. Treating an unknown mode as
   in_place was wrong: execution_mode is how a user asks for ISOLATION, so
   the "safe" fallback would edit the working copy they asked us to stay out
   of. An unrecognised mode now fails the task naming the version skew.

3. Keep Codex conversations alive across turns. The per-issue session store
   was keyed on LocalWorkDir != "", so worktree tasks — which also never
   reuse an env root — got a fresh empty CODEX_HOME per turn and silently
   lost the thread. Covered by a two-turn regression test that fails without
   the fix.

4. Fail closed on an incomplete snapshot. A failed stash create/apply, a
   failed untracked copy, or hitting the copy bounds all degraded to "agent
   sees HEAD" with only a daemon log line. A half-replayed tree looks like a
   working checkout, so nothing downstream questions it while the agent reads
   different code than the user has. All of these now abort the prepare and
   tear the partial worktree back down.

5. Do not snapshot a directory mid-mutation. The same real path can be
   attached to another project as an in_place resource; snapshotting under a
   running in_place task captured a half-written tree plus that task's
   in-flight .agent_context/.multica sidecars, which then reached the branch
   and could feed the agent another issue's brief. Worktree prepare now takes
   the per-path mutex for the snapshot alone (released before the run, so
   tasks still overlap), and the untracked replay skips Multica's own
   sidecars.

6. Stop the test helper from masking regressions. gitRun skipped on any git
   failure, including post-finalize assertions, so a lost branch could report
   as a skip. Git availability is now checked once at repo setup; every later
   git failure is fatal.

Also rebased onto main, keeping both MulticaConfigRoot and LocalWorktree in
the Environment initialiser.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): close sidecar-commit and wait-visibility gaps in worktree mode; gate worktree saves on daemon version (MUL-5707)

- runTask: register the sidecar-cleanup defer immediately after the Finalize
  defer. Previously two early returns (temp-dir setup, StartTask failure) sat
  between them, and on those paths Finalize auto-committed the sidecars
  Prepare had just written — delivering a branch whose only content is
  Multica's own runtime files (in_place: leaving them in the user's tree).
- runTask: give the worktree snapshot wait the same plumbing as the in-place
  acquire — waiting_local_directory status, resourceWaitTasks accounting, and
  a cancellation poller. The holder can be an hours-long in_place task; the
  wait was invisible and a user cancel went unnoticed until the 5-minute
  prepare timeout.
- isMulticaSidecarPath: match sidecar dirs as whole path segments at any
  depth, not only at the repo root. An in_place resource pointed at a
  SUBDIRECTORY leaves its sidecars below that subdirectory; replaying them
  committed another issue's brief into the delivered branch.
- handler: refuse saving execution_mode=worktree while the owning daemon's
  registered cli_version is below MinLocalWorktreeCLIVersion (422,
  daemon_version_unsupported). An old daemon json-skips the field and runs
  tasks IN PLACE — the daemon-side unknown-mode refusal cannot protect a
  daemon that predates the field, so the server has to. Residual (documented):
  a daemon downgraded after the save needs a claim-time gate.
- copyUntrackedFiles: read the -z listing raw instead of TrimSpace-ing it,
  which corrupted filenames beginning/ending with whitespace into paths that
  silently vanished from the replay.
- docs + skill: describe the save-time version gate; drop the stale claim
  that old runtimes fail the task themselves.

Co-authored-by: multica-agent <[email protected]>

* feat(views): UI switch for local_directory execution mode (MUL-5707)

Worktree mode shipped CLI/API-only, which for its target user — someone
running several tasks at once from Desktop — means it does not exist. This
adds the switch to the resources panel.

The choice is framed by what the user gets back, not by the mode
identifiers: "Edit this folder directly" (one task at a time, changes land
in your working copy) vs "Run in parallel, isolated" (tasks run at the same
time and each hands back an agent/… branch). `in_place` / `worktree` stay
visible as a secondary hint for anyone cross-referencing the CLI or docs.

Two entry points, matching where the decision is actually made:

- The add-a-folder flow now asks for the mode after the folder is picked and
  validated, instead of creating the resource and leaving the mode to be
  discovered later.
- Every local_directory row has a mode control. It is deliberately NOT gated
  on being the owning machine — switching the mode only rewrites a field, so
  it works from the web app too, unlike rename or the folder picker.

Both blocked states are handled where the user chooses, not after they
commit:

- Non-git folder: the desktop validator now reports whether the path is
  inside a git working tree (walking up for `.git`, so a subdirectory of a
  repo counts), and the option is disabled with that reason. Unknown — an
  older desktop build, or an existing row — stays permissive; the daemon
  re-checks authoritatively.
- Daemon too old: the version floor is mirrored from the server as
  MIN_LOCAL_WORKTREE_CLI_VERSION and checked against the runtime for the
  resource's OWN daemon, not the machine the browser happens to be on. A
  server rejection is rendered inline in the dialog rather than as a toast,
  so the dialog stays actionable.

A "Parallel" badge on the row is always visible (not hover-only): without it
there is no way to tell whether tasks on a folder edit it directly or hand
back a branch, which is the first thing anyone asks when a task queues.

Also adds `execution_mode` to LocalDirectoryResourceRef — the field existed
server-side but had no TypeScript counterpart — and brings the zh/ja/ko
project-resources docs in line with the English ones, which were the only
locale updated when the mode landed.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon,handler): close worktree isolation and delivery gaps (MUL-5707)

Second review round. Five must-fix items, all on paths where the mode's
promises could still break.

1. A failed sidecar cleanup no longer ships Multica's own files as the
   user's branch. The cleanup errors were logged and Finalize then ran
   `git add -A`, so a permission/disk/lock failure could deliver a branch
   whose entire content was the runtime brief and issue context. Worktree
   mode now escalates a cleanup failure to an abort: nothing is committed,
   the worktree is kept for inspection, and the task fails with the
   preserved path. In place it stays a warning — no commit, no branch, and
   the leftovers are visible in the user's own tree.

2. The untracked replay no longer drops files silently. `Lstat` failures
   and symlinks were `continue`d without counting, so the caller read a
   partial snapshot as complete. Both now count as skipped, which the
   existing fail-closed check turns into a refused prepare. Non-content
   entries (sockets, FIFOs, devices) stay uncounted deliberately.

3. Prepare rolls back a worktree it created. Any later failure — context
   files, provider home, MCP config — returned without an Environment, so
   nothing downstream knew a worktree existed, leaving a registration and
   a branch no task ever ran in. A deferred Discard now runs unless Prepare
   reached its successful return.

4. The daemon-version gate moved to where it can actually hold. Save-time
   validation cannot see a machine downgraded afterwards, and an old daemon
   json-skips execution_mode entirely — it would run the task in place,
   editing the working copy the user asked to isolate. Claims are now
   checked against the runtime that is actually claiming, and a blocked
   task is cancelled with a reason rather than left to be redelivered to
   the same too-old runtime forever.

5. branch_name is delivered end to end. The daemon sent it, but
   TaskCompleteRequest had no field so it vanished at decode, and the
   failure path never sent it at all. It now has a column, travels on both
   terminal callbacks, and renders as a copyable row in Run details. The
   failure path matters most: worktree mode commits the agent's leftovers
   before tearing the worktree down, so a run that died partway still has
   work worth finding.

Docs (all four locales) and the projects-and-resources skill are updated
for the claim-time gate and the branch surface.

Co-authored-by: multica-agent <[email protected]>

* fix(handler,daemon): migration renumber, cancel reasons, branch on every terminal path (MUL-5707)

Third review round.

1. Rebased onto main and renumbered the migration to 285 — main had taken
   273 and is now at 284. sqlc regenerated on top of the rebased queries;
   the conflicts were confined to generated files.

2. The claim gate now persists why it cancelled. It called CancelTask,
   which writes only status='cancelled', and returned the reason as a 4xx
   that reaches the daemon's log and nothing else — batch claim drops the
   task from its response entirely. A run refused for a durable reason the
   user has to act on cannot present as a bare "cancelled", so this adds
   CancelAgentTaskWithReason and records error + failure_reason on the row.

3. branch_name now survives every terminal path, not just the two direct
   callbacks. Three were dropping it:
   - the context-exhausted complete→fail reroute did not copy it;
   - the daemon's complete→fail fallback (server rejected the complete,
     agent had succeeded) did not set it;
   - both cancellation paths discarded the result wholesale, even though
     the worktree is finalized — and its work committed — before the daemon
     learns of the cancel. The branch now rides the cancel-ack and is
     recorded by SetAgentTaskBranchName, which never overwrites a name a
     terminal callback already wrote.

4. Rollout/rollback compatibility. Isolation is enforced by two server-side
   gates, so a daemon downgraded at any point after the resource was saved
   is caught. The gap they cannot close is rolling the SERVER back past this
   change while sub-floor daemons are still connected: an old server has no
   gate and an old daemon ignores execution_mode. That constraint is now
   documented where an operator will meet it (the migration and the docs),
   and the version floor is pinned across server and frontend by test so the
   two gates cannot silently drift apart.

Co-authored-by: multica-agent <[email protected]>

* fix(server,views): route claim-gate cancels through TaskService, make cancel reasons visible, harden cancel-ack branch delivery (MUL-5707)

Closes the fourth-round review findings on worktree mode's cancellation
chain:

- Claim gate: cancel through a new TaskService.CancelTaskWithReason instead
  of a raw CancelAgentTaskWithReason query, so a gate-cancelled task gets the
  full cancellation side effects — audit capture, chat settle, agent status
  reconcile, task:cancelled broadcast, NotifyTaskFinished. Added REAL claim
  integration tests: singular claim (422 + row cancelled with reason), batch
  claim (task omitted + row cancelled with reason), and a current-runtime
  control.
- Cancel reasons are now user-visible: execution log rows, agent activity
  rows, and the transcript dialog label a SERVER-cancelled task (persisted
  error/failure_reason) like a failed one, with the actionable text on hover
  and a Reason row in Run details. User-initiated cancels stay a plain
  "Cancelled". New cancelReasonLabel helper + local_directory_error label;
  details_reason locale key in en/zh-Hans/ja/ko; widened
  AgentTask.failure_reason to the open string the wire actually carries.
- Cancel-ack is now a terminal delivery: the daemon retries it on transient
  errors (same schedule as complete/fail), the server fails LOUD (5xx) when
  persisting the branch/error fails instead of warn-and-200, and re-broadcasts
  task:cancelled after a successful write so clients that already refetched a
  branchless row pick up the delivery. Replays are idempotent (never
  overwrite).
- Cancel + Finalize-abort no longer swallows the preserved-worktree pointer:
  the finalize error is wrapped in worktreePreservedError (joined onto an
  existing run error, never displacing it), extracted on the cancel path, and
  delivered via the ack into agent_task_queue.error through the new
  SetAgentTaskErrorIfEmpty query — never overwriting a reason recorded by a
  fail callback or the claim gate. Other run errors on a cancelled run stay
  discarded as before: they are expected noise.
- Docs: the self-hosted rollback constraint callout now exists in zh/ja/ko,
  not just English.

Co-authored-by: multica-agent <[email protected]>

* fix(handler): guard cancel-ack writes to cancelled rows; requeue when the claim gate's cancel fails (MUL-5707)

Fifth-round review fixes:

- SetAgentTaskBranchName / SetAgentTaskErrorIfEmpty now carry a
  status='cancelled' CAS. The daemon acks on EVERY terminal status it
  observes, so a late or replayed ack from a stale run could previously
  backfill its branch or preserved-path error into a completed/failed row
  whose own callback — the authoritative channel — recorded nothing, without
  any event invalidating the change. A CAS-refused write is a deliberate
  no-op (still 200: nothing to retry), and the rebroadcast stays guarded by
  the same status check. Regression tests: a late ack against a completed row
  with empty fields and against a failed row leaves both untouched.
- When the worktree claim gate's CancelTaskWithReason fails, the claimed row
  no longer strands in dispatched (daemon refused, nothing to release it but
  stale reclaim). The gate requeues via RequeueTaskAfterClaimFailure — the
  next claim re-runs the gate and retries the cancel — and reports 5xx so the
  singular daemon reads a transient server problem, not a claim refusal; the
  batch path omits the task as before, now with the row requeued. Fault
  injection tests (row-scoped trigger raising on the cancel UPDATE while
  letting the requeue through) cover singular and batch.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-13 14:52:01 +08:00
mxbao063-baoandmxbao063-bao 8060aa7277 MUL-5947: feat(dingtalk): route groups to different agents (#6660)
Route each DingTalk group discovered by one Stream robot to its own agent, with a revision-fenced cutover so a reassigned group never inherits the previous agent's session, transcript, or pending outbound card.

Co-authored-by: mxbao063-bao <[email protected]>
2026-08-13 14:09:25 +08:00
e4ebd41def fix(daemon): make the daemon log path discoverable (#6884)
Debugging a daemon meant guessing which daemon.log was live. In #6038 an
external reporter followed the docs to ~/.multica/daemon.log, which had
been stale for 17 hours (a leftover default-profile daemon) but read
perfectly — the worst kind of wrong file. He only found the real log by
ignoring the instructions and digging through the directory.

Two causes, both in this change:

- The docs only described the default profile's path. The real layout is
  ProfileDir(profile)/daemon.log, so a named profile — what a Desktop
  install normally runs on — logs to ~/.multica/profiles/<name>/. Document
  both, along with daemon.pid and daemon.err.log which sit beside it.

- `multica daemon logs` revealed the resolved path only on the
  file-not-found error path; the success path tailed silently. It now
  prints the absolute path and profile label it resolved. That goes to
  stderr, before the tail starts, so it also shows under -f and a pipe or
  redirect still carries log content only.

Resolution is factored into daemonLogSourcePath, which fails loudly rather
than returning a bare "daemon.log" that would resolve against the caller's
cwd — a printed path has to be one you can actually open.

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-13 13:59:54 +08:00
GuillaumeandGuillaume Sayer 147cd8d84f feat(skills): update imported skills from their source (#6847)
A skill imported from GitHub, skills.sh, or ClawHub can now be re-downloaded
in place via POST /api/skills/{id}/refresh, the Update button on the skill
page / list row menu, or 'multica skill refresh <id>'. The skill id,
created_by, created_at, labels, and agent_skill bindings are preserved, so
agents keep the skill without re-adding it; name (upstream renames are
adopted, 409 on collision), description, SKILL.md, files, and config.origin
are replaced. Permission follows canManageSkill (creator or workspace
owner/admin); non-refreshable origins (manual, archive, runtime_local)
return 422. Custom config keys are preserved by merging only the origin key.

Co-authored-by: Guillaume Sayer <[email protected]>
2026-08-13 13:48:18 +08:00
Bowserandmultica-agent 254a02a795 MUL-5799: feat(daemon): allow relocating per-task temp dirs via MULTICA_AGENT_TEMP_BASE (#6481)
* feat(daemon): allow relocating per-task temp dirs via MULTICA_AGENT_TEMP_BASE (#6475)

On Linux/macOS the private per-task temp dir was always created under
/tmp and the daemon's own TMPDIR was ignored, so a self-hosted operator
could not move agent temp data to a larger disk.

When MULTICA_AGENT_TEMP_BASE is set, it must be an absolute path to an
existing directory and is used as the MkdirTemp base; invalid values
fail task startup with a clear error instead of silently falling back.
Unset keeps the previous platform default byte-for-byte. Random naming,
0700 permissions, TMPDIR/TMP/TEMP injection, the agent custom_env
blocklist, and task-exit cleanup are unchanged. Windows is untouched.

Co-authored-by: multica-agent <[email protected]>

* feat(daemon): validate MULTICA_AGENT_TEMP_BASE is writable (#6475)

An existing directory the daemon cannot write to still failed task
startup, but as a bare mkdir error from deeper in the run. Probe
writability while resolving the base so the failure names the variable
the operator set. The probe runs only when the variable is set, leaving
the default /tmp path byte-for-byte unchanged.

Cover the contract at the level operators experience it: an unusable
base fails runTask with a message naming the variable, and the agent
never starts. Also restore the environment in the unset subtest, which
previously called os.Unsetenv without a matching restore and could leak
into later tests in the package.

Co-authored-by: multica-agent <[email protected]>

* chore: remove accidentally committed commitmsg.txt (#6475)

A commit message draft was swept into the previous commit by git add -A. It is not part of the product surface and does not belong in the tree.

Co-authored-by: multica-agent <[email protected]>

* docs: scope the sun_path limit to Unix-socket platforms (#6475)

The 108/104 byte sun_path ceiling only constrains platforms that use Unix domain sockets; the previous phrasing read as a cross-platform rule.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): address temp base review feedback (#6475)

Keep MULTICA_AGENT_TEMP_BASE Unix-only, use the real task directory creation as the sole writability check, and wrap configured-base failures with the variable name. Remove the diagnostic probe and its possible residue path.

Add the missing localized documentation and pin private task directory permissions at 0700 in tests.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
2026-08-13 12:23:59 +08:00
5e569fa38a fix(workspace): derive new issue prefixes from the slug, editable at create (#6815)
Every workspace named only in Chinese (or Japanese, Korean, emoji…) was
created with the issue prefix "WS": the server derived it from the
workspace NAME by stripping non-ASCII letters, and a CJK name strips to
nothing. Not a collision risk — a certainty, for every such team, with no
way to change it in the create flow.

New workspaces now derive their default prefix from the slug, which the
same form already forces the user to pick in ASCII, and the onboarding
field is editable instead of a read-only preview. The client preview and
the server derivation are now the same algorithm, so the create screen
stops promising an identifier the user won't get.

Existing workspaces are untouched by design: no backfill, and the
read-time fallback for rows with an empty prefix keeps the old
name-based derivation (now legacyIssuePrefixFromName, frozen and
test-guarded). Identifiers are computed at read time, so moving that
path would have rewritten every historical issue identifier in those
workspaces.

Also validates issue_prefix as ^[A-Z0-9]{1,10}$ on create and update.
Settings guarded this client-side, but the API took anything — CJK text
or a 100-character string went straight into the column.

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-12 15:45:07 +08:00
3ef345b658 fix(cli): allow login with stale daemon port env (#6801)
* fix(cli): allow login with stale daemon port env

Co-authored-by: multica-agent <[email protected]>

* fix(cli): address daemon port review findings

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-12 13:19:05 +08:00