Commit Graph
1517 Commits
Author SHA1 Message Date
d276991c60 MUL-6743 refactor(issues): remove the assignment handoff note (#7626)
* refactor(issues): remove the assignment handoff note (MUL-6743)

Usage data puts the handoff note at 0.2% of assignments, so the box
occupied the confirm dialog's centre for the other 99.8% and bought
them nothing. Removing it takes the whole feature with it: the note
input and its runtime version gate, the `handoff_note` write field on
PATCH/batch issue updates, the task column write, and the daemon's
opening-prompt + issue_context.md handoff sections.

The confirm dialog itself stays — it still names who is about to run
and still offers "Don't start yet" (suppress_run).

The `*WithHandoff` enqueue methods only ever varied by that note, so
they become `EnqueueTaskForIssueByActor` / `EnqueueTaskForSquadLeaderByActor`
— the actor-carrying variants autopilot already called with an empty
note. `preview-trigger` drops `handoff_supported`, which nothing read
after the gate moved client-side.

`agent_task_queue.handoff_note` is deliberately left in place: it holds
historical rows and this is the expand half of an expand-contract pair.
Nothing reads or writes it now, so a follow-up migration can drop it
once no deployed binary selects it.

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

* docs(issues): reshoot the assignment confirm dialog (MUL-6743)

The asset still showed the handoff note box the surrounding prose no
longer describes. Recaptured from the app at the same 600x450 frame;
the confirm button now carries the send-chord keycaps it has shipped
with since MUL-5694.

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

---------

Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-03 16:13:33 +08:00
Damao d2bc574ff2 MUL-6987: docs: document task runtime environment contract (#7956)
Document the daemon-injected task runtime environment contract, stability boundary, attribution behavior, and safe child-process token handling across English, Chinese, Japanese, and Korean docs.
2026-09-03 16:00:07 +08:00
Bohan Jiangandmultica-agent 2c478897c7 docs(providers): mark Oh-My-Pi as Multica-managed MCP (MUL-6997) (#7965)
The comparison table still showed `—` in the "Multica-managed MCP" column for
Oh-My-Pi (`omp`) across all four locales. That has been stale since #7565, which
added `omp` to `MCP_SUPPORTED_PROVIDERS` and materialises the managed config as
`{workDir}/.omp/mcp.json` via `prepareOmpMcpConfig`.

The MCP configuration section further down the same pages was already correct —
it lists only Antigravity, GitHub Copilot CLI, DevEco Code, and Pi as runtimes
that do not read an agent's MCP config — so the table row was contradicting the
prose on its own page.

Every other row in that column was re-checked against `MCP_SUPPORTED_PROVIDERS`
in `packages/core/agents/mcp-support.ts`; `omp` was the only mismatch.

Co-authored-by: multica-agent <[email protected]>
2026-09-03 14:14:03 +08:00
Soda 0431218061 MUL-6833 fix(web): localize auth callback and error pages (#7752)
Localize the web auth callback, 404 page, and root error boundary, plus the shared section-level ErrorBoundary.

Also fixes several pre-existing auth-path bugs surfaced during review:
- access_denied was unreachable (the !code branch preceded the error-param check)
- UserLocaleSync's reload could replay a consumed OAuth code and show 'Login Failed' after a successful sign-in
- Google token-exchange and userinfo provider failures were misreported as user-actionable causes
- checkSignupAllowed reported 'registration is disabled' for an allowlist miss
2026-09-03 14:13:47 +08:00
ASDFGHoneyandmultica-agent 01dbfae45c feat(desktop): add numbered tab shortcuts (MUL-6923) (#7870)
* feat(desktop): add numbered tab shortcuts

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

* fix(desktop): complete numbered tab shortcut support

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

* fix(desktop): align tab shortcuts with logical keys

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

---------

Co-authored-by: multica-agent <[email protected]>
2026-09-03 13:49:20 +08:00
603519d158 MUL-6977: test: remove implementation-only coverage (#7932)
* test: remove implementation-only coverage

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

* test: remove remaining implementation-only coverage

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

* test: restore compatibility and fragment navigation coverage

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

* test: preserve behavioral cleanup contracts

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-03 13:23:25 +08:00
Firer d4a712abf3 MUL-6771 feat(cli): filter and sort issue list by custom property (#7664)
Expose the `properties=` filter and `sort=property:<id>` that GET /api/issues
already implemented but the CLI never surfaced, so scripts no longer have to
page through everything and filter locally:

  multica issue list --property "Impact=High" --property "Impact=Medium"
  multica issue list --property "QA Status=__none__" --status in_review
  multica issue list --sort property:Impact --direction desc

--property takes one Name=Value per flag; repeating a property ORs its values,
different properties AND. Values resolve by name the way the rest of the
property CLI works. __none__ is the reserved unset filter. A value the server
could never have stored — a non-http(s) url, over-long text, a non-numeric
number, a date in another format — is rejected up front rather than returning
the empty page that reads like a real answer. The >=, <= and != spellings are
reserved so comparison filters can be added later without breaking scripts.

Also fixes propertySortExpr, which sorted a select property by the raw stored
value — the option-id UUID, an arbitrary order. Values now rank by their
position in the definition's option list, so an ordinal scale sorts by meaning
(Low < Medium < High), matching what board grouping already treats as
canonical.

Closes #7663
2026-09-02 18:42:25 +08:00
d7247fa9fc MUL-6975: preserve departed member identity in timelines (#7931)
* fix(timeline): preserve departed member identity

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

* fix(timeline): preserve identity across live updates

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-02 17:58:14 +08:00
47dc75741c docs(changelog): add v0.4.38 release entry (2026-09-02) (MUL-6978) (#7930)
* docs(changelog): add v0.4.38 release entry (2026-09-02) (MUL-6978)

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

* docs(changelog): drop unreleased Autopilot quota lines and rewrite v0.4.38 titles (MUL-6978)

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-02 17:43:44 +08:00
a2d819ea40 MUL-6962 fix(daemon): give environment preparation failures their own failure reason (#7915)
* fix(daemon): give environment preparation failures their own failure reason

A failed execenv.Prepare / Reuse is a filesystem error on the daemon's own
host, but handleTask routed it through taskfailure.Classify — a classifier
written to read agent and provider output — so a full disk, a denied
permission or a directory held open by another process all landed in
agent_error.*: the catchall today, provider_server_error on the report that
opened the issue and sent hours of diagnosis at an LLM vendor.

The daemon now tags those errors structurally (environmentSetupError) and
reports the new platform-side reason environment_prepare_failed. The four
existing sentinels still win: task identity, the prepare budget, skill
downloads and the OpenClaw CLI deadline each name a specific cause inside the
same phase.

Installed daemons update on their own cadence, and they report a non-empty
reason that FailTask deliberately leaves alone, so NormalizeDaemonReason
upgrades any agent_error.* value whose error text opens with the prepare /
reuse wrapper. That witness proves the run died before an agent existed, so
every label in that namespace is wrong by construction.

Not retryable and resume-safe: a full disk reproduces identically on the next
attempt, and no session was touched.

Closes #7913

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

* fix(mobile): parse failure_reason as an open string, and match copy to real scope

Review follow-up on #7915.

AgentTaskSchema pinned failure_reason to the six pre-MUL-1949 coarse values
with `.catch("")`, so every refined reason was erased to undefined before any
surface read it. run-row's badge map was widened in MUL-5370 and has been
unreachable ever since: agent_error.*, skill_bundle_unavailable and the new
environment_prepare_failed all rendered a bare "Failed". The field is an open
string on the wire (packages/core/types/agent.ts) and in the chat message
schema beside it; the task schema now matches, and the badge map moved to
lib/run-failure-badge.ts so mobile's node-only vitest lane can cover the
lookup a component test cannot reach.

execenv.Prepare is not only filesystem work — it also writes and validates the
per-provider local config (Codex home, Hermes overlay, Cursor MCP, and an
OpenClaw config it fails closed on), so a malformed local config lands on this
reason too. The Go docs, the chat copy in four locales and the docs table said
"disk, permission, directory in use", which would send the reader past half
the real causes — the same misdirection this reason exists to end. They now
point at the raw error first and name the config case, with regression tests
for a non-filesystem preparation failure on both the daemon and the legacy
wire path.

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-02 14:45:41 +08:00
10f4f8e938 MUL-6957 fix(daemon): log full task ids so concurrent runs stay distinguishable (#7910)
* fix(daemon): log full task ids so concurrent runs stay distinguishable

The workdir isolation fix for #7326 gave same-prefix tasks separate env
roots, but the daemon log still rendered every task id as its first eight
hex chars. Task ids are UUIDv7: those chars are the high 32 bits of a
millisecond timestamp and only advance every ~65s, so any two tasks
started in the same window print an identical `task=` value. Two healthy
concurrent runs were indistinguishable in the log exactly when scheduling,
provider or workdir behaviour needed to be traced per run.

Structured log fields (`task`, `chat_session`, `holder`, `target_task`)
now carry the whole id — the same key the task JSON, the env-root
ownership manifest and the server logs print, so a log line can be joined
to them. shortID stays for the one human-facing label that is read rather
than joined (the "waiting for local_directory (held by task …)" reason the
UI renders), with a doc comment fencing it off from log fields.

Two follow-ons: the idle watchdog no longer passes a second `task` field
that duplicated the one its task-scoped logger already carries, and the
desktop log panel's collapsed field budget goes from 32 to 36 chars so a
canonical UUID is not clipped at the entropy end of the value.

Verified: new regression tests reproduce the reported `task=01a05ec1` for
both field ids and pass on the fix; the rest of the daemon suite is
unchanged (same 7 pre-existing environment failures before and after).

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

* refactor(daemon): drop the duplicate task field on the reuse-mismatch log

Review nit on #7910: the "reused workdir is not the directory that was
claimed" line passed "task" into taskLog, which handleTask has already
bound the full id to, so the id printed twice on that line — 72 chars of
duplicate now that the value is a whole UUID. Same shape as the idle
watchdog case already cleaned up in the parent commit; this was the last
one in the package.

Guarded from both sides. The behavioural test now asserts no emitted line
carries more than one `task=` field, which covers everything handleTask
reaches. Most taskLog calls live deeper inside runTask behind a real agent
process, so a structural test parses the package sources and fails on any
taskLog.<Level>(...) passing a literal "task" key, following the AST-scan
precedent in agent_command_names_test.go — including its lesson: the scan
asserts it still matches at least 20 calls (it sees 81) so a rename or a
file move fails loudly instead of passing vacuously.

Verified: both guards fail with precise file:line output when the
duplicate is reintroduced, and pass on the fix. Daemon suite unchanged
(same pre-existing environment failures).

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-02 13:37:41 +08:00
3c23c491fa MUL-6924: feat(billing): notify workspaces about autopilot quota (#7681)
* feat(billing): notify workspaces about autopilot quota

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

* fix(billing): isolate and throttle quota notices

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

* fix(billing): tighten quota notice delivery

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

* fix(billing): reuse quota admission snapshot

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

* fix(autopilot): make quota notices actionable

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

* fix(billing): align quota recovery notifications

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

* fix(billing): align quota notice recipients

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

* fix(billing): finish quota notice review

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

---------

Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Sol-Boy <[email protected]>
2026-09-02 12:59:26 +08:00
iiwish 5bc021c369 MUL-6542 fix(mobile): stabilize recycled markdown layout (#7499) 2026-09-01 21:14:47 +08:00
310cfdc1e5 MUL-6896 fix(i18n): finish the MUL-5703 glossary rollout across ja/ko and docs (#7867)
* fix(i18n): finish the MUL-5703 glossary rollout across ja/ko and docs (MUL-6896)

MUL-5703 settled the rule — the issue entity is translated (任务 / タスク /
태스크) and the agent run keeps `task` / 作業 / 작업 — but only ever landed in
the zh-Hans UI. Everything else drifted:

- ja/ko shipped 39 UI strings that still spelled the entity in English
  (`issue`), or as イシュー / 課題 / 이슈.
- zh and ja spelled the *run* with the *issue* word in the project worktree
  copy and in the MCP delete dialog — the exact collision MUL-5703 exists to
  prevent. ko already had this right (작업).
- ko wrote runtime-health "issues" as 이슈, so a machine card read as the
  entity. zh/ja use 异常 / 問題, and ko's own health_clear already said 문제.
- `conventions.ja.mdx` / `conventions.ko.mdx` still documented the *pre*-
  MUL-5703 rule ("issue stays English"), contradicting the en/zh glossary.
- The docs prose sweep the glossary calls "being swept separately" never
  happened: ~471 English `issue` in zh, ~414 イシュー in ja, ~418 이슈 in ko.

Sweeping the entity into 任务 / タスク / 태스크 forces the run to move too —
zh docs wrote it 执行任务, ja 実行タスク, ko 실행 태스크, all of which collide
with the entity word once the entity lands there. So the run is rewritten to
`task` / 作業 / 작업 in the same pass, matching what each UI already shows.
Standalone labels keep the English source's capitalisation (`Task` page
title and links); inline mentions are lowercase, like `skill`.

Also corrects the en source for two squad strings: squads receive *issues*
and the leader delegates *sub-issues*, not "tasks" / "sub-tasks". The three
translations already said the entity, so only en moves.

Not touched: the landing changelog archive, the generic English noun "task"
in chat/onboarding marketing copy, `issue` in the "problem" sense, and the
unrelated structural drift in the ja/ko conventions pages (they are missing
the "Runtime display names" section from an older sync).

Verified: 4907 views tests pass including locale parity, typecheck and lint
clean, go build plus the integrations/cli suites pass, and no doc anchor
broke (checked before/after).

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

* fix(docs): sweep the last old run-word leftovers in zh copy (MUL-6896)

Review caught the docs home page still linking "[执行任务](/tasks)" while ja/ko
had already moved to 作業 / 작업. Four more of the same class turned up next to
it: the same link in README.zh, "任务生命周期" in VISION.zh (en: "the task
lifecycle"), and two redundant "task(task)" glosses the sweep produced where
the source had written "执行任务(task)".

With these, `执行任务` / `実行タスク` / `실행 태스크` are gone repo-wide, so the
"prose now follows the table" line added to conventions.mdx is true.

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-01 16:04:34 +08:00
61ea48fd2e MUL-6881 fix(daemon): keep one worktree branch per conversation in local worktree mode (#7815)
* fix(daemon): keep one worktree branch per conversation in local worktree mode

Every comment on a local_directory issue started a new task, and worktree
mode gave each one a branch of its own forked from the user's HEAD. Turn two
therefore stood in a tree that did not contain turn one's work, with that work
stranded on another branch and nothing saying so.

Tasks that share a conversation — the turns of one issue, one chat session —
now share a branch, agent/<agent>/<issue>, and a follow-up checks it out at its
tip instead of forking from HEAD. Env roots stay per task, so sibling tasks
still run concurrently; when one already holds the conversation's branch, the
other forks from its tip onto agent/<agent>/<issue>-<task> rather than falling
back to HEAD.

Replaying the user's uncommitted tree onto a continued branch cannot use the
turn-one path: that merge takes the user's HEAD as its base, so it conflicts
exactly when the agent edited the lines the user had left uncommitted. Each
turn now records the snapshot it saw at refs/multica/local-state/<branch> and
the next one replays only the difference — a cherry-pick whose parent is that
snapshot. A genuine conflict leaves the branch as it stands and logs which
files did not come across, rather than stranding the conversation.

Teardown follows the same rule everywhere: a branch this prepare did not
create belongs to the turns before it, so Finalize's read-only cleanup, Discard
and the prepare rollbacks only ever drop a branch they created themselves.

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

* fix(daemon): prove branch ownership and hand replay conflicts to the agent

Review of the per-conversation worktree branch found four ways it could put
the wrong tree in front of the agent. All four came from the same two gaps:
the user's snapshot did not describe their whole directory, and a branch was
recognised by its name.

The snapshot is now one commit covering tracked edits AND untracked files,
built in a private index inside the task's env root. It answers the question a
continued branch actually has to ask — what has the user changed since the
state I already carry — for every file, so a once-untracked file the agent
edited is no longer reverted to the user's older copy each turn, and their
later deletion of one carries. Redirecting GIT_INDEX_FILE also removes the
.git/index.lock race the previous capture had to retry around (#7434).

A conflict between the user's newer edits and the branch's own work is now the
agent's to resolve, per the product decision on the issue: the worktree is
handed over mid-merge, the turn's prompt names the files and says which side is
which, and Finalize refuses to deliver — preserving the worktree — while
anything is still unmerged. The snapshot only advances once the branch really
carries it, so an unresolved edit is offered again next turn instead of being
recorded as synced. Before this, the conflict was reset away and the user's
change was lost from every later turn.

Continuing a branch is decided by an ownership record — workspace, agent and
conversation ids, recorded in the snapshot commit at
refs/multica/local-state/<branch> — never by the branch name. A same-named
branch the user created is left untouched and the task takes
agent/<agent>/<issue>-<fingerprint>, which its own follow-ups then continue.
The same check keeps two workspaces that mint one issue identifier apart.

Those refs are now GC'd: every prepare drops the record of a branch that no
longer exists, and the docs say where they live and how to clear them.

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

* fix(daemon): tie a task branch's record to the branch, and quote untrusted paths

Ownership proved that Multica once wrote a branch of this name, which stayed
true after the user deleted it and created their own under the same name. No
prepare runs in that gap, so the orphan sweep never sees the branch missing and
the next turn continued straight into the user's unrelated work.

The record now also pins the branch tip it was written at, as the second parent
of the record commit, and a branch is only continued while that commit is still
an ancestor of its tip. Deleted-and-recreated, force-moved and rebased branches
all fail that test; committing on top of a delivered branch — the normal case —
passes it and picks the user's commit up. Finalize re-records afterwards,
because a checkpoint written at prepare time sits on the branch's base, which is
exactly where a recreated branch would sit.

Conflicted paths reaching the turn's prompt are quoted rather than wrapped in a
code span. They come from the user's repository and git allows newlines,
backticks and quotes in a filename, so a raw one could close its list item and
continue as an instruction line of its own; the same quoting is applied to the
Finalize error. The list is bounded at 50 names with the remainder counted.

Also drops a duplicated pair of bullets in the Chinese docs and corrects two
file comments that still described the stash-and-copy replay.

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

* fix(daemon): make the branch record part of delivery, and bound the conflict list by bytes

The record could still authorise a branch that was not ours. Finalize wrote it
after removing the worktree and treated a failure as non-fatal, so a failed
update-ref left the ref pointing at the record Prepare had written — whose
checkpoint is the branch's base, which is exactly where a branch the user
deletes and recreates from HEAD sits. The ancestor check then passed on their
work.

Two changes close it. A checkpoint is only written when it is a commit the
branch could not sit at without this conversation's work: Finalize records the
tip it actually delivered, and Prepare records early only once the branch has
moved off the user's HEAD — which is the baseline commit, or a continued
branch's own tip. And the record is now part of delivering: Finalize writes it
BEFORE removing the worktree and fails the task if it cannot, so the worktree is
still there to preserve, exactly as for a commit that could not be made. The
checkpoint is the commit the caller delivered rather than a re-read of the
branch ref, so a branch moved in that window fails the next ancestor check
instead of being recorded as current.

A sibling racing the very first turn of a conversation on a clean tree now finds
no record yet and starts its own line of work rather than forking from a branch
it cannot prove is ours. That is the price of the rule and it is the safe side.

The conflict list in the prompt is bounded by rendered bytes rather than by
entries — a git path is as long as the filesystem allows, so 50 entries bounded
nothing — with the remainder counted and a single oversized path unable to
overrun the budget either.

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

* fix(daemon): give every task branch a commit of its own, and verify what a run delivers

The checkpoint rule — record only a commit the branch could not sit at without
this conversation's work — held in Prepare and not in Finalize. Finalize took
whatever the worktree's HEAD was, so a run that reset its worktree back to the
user's own HEAD had that plain user commit recorded as the branch's checkpoint,
and a branch the user later recreated there passed the ownership check.

Two changes make the rule total. Every branch this mode creates now gets a
baseline commit of its own, empty when the user's tree was clean: without it the
branch sits exactly where the user's HEAD does and nothing tells it apart from a
branch they create at the same place. It also makes `git diff <baseline>..
<branch>` the agent's work on every branch rather than only on the ones that
started dirty, which is what the surrounding comments already claimed.

And Finalize verifies the delivery point before recording it: the tip has to BE
the task's branch — not a detached checkout, not a branch someone moved
underneath the run — and it has to still contain the anchor, which is that
baseline or the checkpoint the previous turn recorded and this one verified. A
delivery that fails either test is not recorded at all: the task fails, the
worktree is preserved, and the error names what is missing.

A sibling racing the first turn of a conversation forks from it again, since
every branch now carries a recordable checkpoint from the moment it is prepared.

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

* fix(daemon): keep the anchor on the commit that carries this turn's user edits

A follow-up turn commits the user's newest edits as its own baseline, but the
anchor stayed on the checkpoint the previous turn had recorded. A run that then
reset its worktree back to the previous tip still passed verification — the old
checkpoint is an ancestor of it — so the turn was recorded as having delivered
edits the branch no longer had. The turn after that computed its replay from a
state the branch never took, and the user's edits were simply absent from the
worktree with nothing to indicate it.

The anchor now follows every baseline this task commits, so it always names the
newest commit carrying the state about to be recorded. Resetting past it fails
the turn and preserves the worktree, as for the first-turn case.

The conflict path had the same hole from the other end: there the branch does
not carry the snapshot until the agent resolves and something is committed. When
a turn that started mid-merge ends with the branch exactly where it began, the
record keeps the state the branch is known to carry instead of advancing, and
the next turn offers those edits again. A resolution in favour of the branch's
own side leaves the same empty trace as discarding the merge, so this trades a
merge that may have to be concluded twice against recording edits as delivered
that may have been dropped.

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

* fix(daemon): measure a delivery against the commit its own turn started from

A turn that starts mid-merge only carries the user's edits once the agent
resolves and something is committed, and the guard for that compared the
delivered tip against the checkpoint an earlier turn had recorded. Those are not
the same commit: the user committing on a delivered branch is supported and
tested, so the branch tip a later turn starts from can already be past that
checkpoint. A resolution that committed nothing then left the tip equal to the
turn's own starting point but not to the older checkpoint, the guard did not
fire, and the edits were recorded as delivered — missing from the turn after
with nothing to indicate it.

Both questions now measure against LocalWorktree.BaseCommit, the commit this
turn actually started from. It is at least as new as the earlier checkpoint in
every case, so the separate anchor field is gone: a delivered tip that contains
this turn's starting point contains everything an earlier turn recorded, and a
tip that does not is a run that reset away work already delivered.

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

* test(daemon): cover the claim-to-branch wiring with a two-turn Prepare test

Every multi-turn test so far drove PrepareLocalWorktree with hand-filled
params, which skips the step that turns a claim into them: which field becomes
the branch name, and which become the identity the branch is recorded under.
This one runs two turns of one issue through Prepare itself, and a third for
another workspace carrying the same issue identifier. Both halves of that
mapping are load-bearing — blanking the conversation key or fixing the
workspace id makes it fail.

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

* fix(daemon): carry the worktree's own state across the preparation helper

Prepare runs in a short-lived helper process and the Environment it built comes
back to the daemon as JSON. LocalWorktree keeps everything Finalize reasons
about in unexported fields, and ordinary struct marshalling drops those without
a word — so the daemon finalized a worktree whose owner, snapshot, tracksState
and createdBranch were all zero.

Nothing failed; the guarantees were simply absent in production. No delivery
verification ran, Finalize recorded nothing (leaving the checkpoint at the
baseline Prepare had written inside the helper), a conflicted turn could not
hold its snapshot back, and a read-only turn kept its branch. Every in-process
test passed throughout, because they call PrepareLocalWorktree directly.

Custom Marshal/Unmarshal now carry that state. aborted stays out of the wire
deliberately: the daemon sets it after Prepare returns, so it belongs to the
parent. The regression tests run two turns through PrepareIsolated — the real
boundary — and assert the state arrives, that Finalize's record names the tip it
delivered rather than the baseline, and that a read-only turn still drops its
branch.

Found by running the whole stack: a real daemon, a real local_directory in
worktree mode, and two real turns.

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-01 13:50:52 +08:00
Soda b69b13b037 fix(desktop): localize daemon controls (#7749) 2026-09-01 12:19:52 +08:00
Danila Katalshov 11861145ab MUL-6758 feat(core): add truncateWithEllipsis string helper (#7646)
Adds `truncateWithEllipsis(text, maxLength)` to packages/core/utils.ts and
routes two existing truncation sites through it.

The helper appends the single-character ellipsis `…` (U+2026), counts it
toward the budget, and measures that budget in Unicode code points rather
than UTF-16 code units, so an emoji is never cut into a lone surrogate.
Trailing whitespace is trimmed before the ellipsis is appended.

Adopted by the task-transcript trace presenter and the desktop daemon
panel. Both previously used a content-character budget, so each passes
`max + 1`. Content of length exactly `max + 1` is now shown in full rather
than truncated — the old code spent an ellipsis to hide a single character.

Closes #7544.

Co-authored-by: Danila Katalshov <[email protected]>
2026-08-31 20:37:46 +08:00
79559ebb92 fix(ci): unblock the release by fixing two flaky main failures (MUL-6890) (#7828)
* test(wecom): move the relay claim tests off a shared Redis DB index (MUL-6890)

The relay outcome tests keep their claims in Redis DB 12, which
internal/service also takes for its own suite — and both flush that DB on
entry and exit. `go test ./...` runs packages concurrently, so a flush from
one package can delete a live delivery claim held by the other, and the
publisher's outcome watch then reads an absent claim as "nobody delivered"
and counts a drop for a reply the holder had already sent.

That is the intermittent `outbound_dropped on the publisher = 1, want 0 —
it was delivered` on main. Reproduced by running internal/service's
Redis-backed tests alongside this one; fixed by giving the package an index
nothing else uses, with the current allocation written down so the next one
does not collide either.

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

* build(turbo): generate fumadocs .source once instead of racing (MUL-6890)

`build` and `typecheck` both ran `fumadocs-mdx` before their real work, and
turbo is free to run those two tasks for the same package at the same time.
fumadocs-mdx regenerates `.source/` by removing the directory and writing it
back, so the two processes fight over one tree: whichever one mkdir's first
can have it deleted underneath, and its write dies on `ENOENT: no such file
or directory, open '.source/index.ts'` — the frontend-build failure on main,
which is pure timing and passes on a rerun.

Move the generation into its own `mdx` task that both depend on. It runs
once, its output is cached like any other task's, and the two consumers can
still run concurrently. Packages with no `mdx` script are unaffected: turbo
skips a dependency nobody implements, the same way `cache-inputs` already
works here.

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-31 18:29:37 +08:00
0742b50428 docs(changelog): add v0.4.37 release entry (2026-08-31) (MUL-6890) (#7823)
* docs(changelog): add v0.4.37 release entry (2026-08-31) (MUL-6890)

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

* docs(changelog): retitle v0.4.37 around the day's optimizations (MUL-6890)

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-31 18:05:45 +08:00
mrlonely 2ebe0ae327 MUL-6862: fix(codex): widen thread setup handshake budget (#7790)
* fix(codex): widen thread setup handshake budget

* fix(codex): preserve handshake timeout overrides
2026-08-31 17:02:24 +08:00
Xichang(Seacen) Zhao 1dd6b9ecdb MUL-6570 feat(wecom): route a reply to the replica that holds the bot's socket (#7429)
WeCom is the only channel with no outbound REST path: every write goes over the aibot WebSocket, and the WS lease means exactly one replica holds it. `chat:done` is published on the in-process bus by whichever replica served the daemon's completion — a load-balancer decision, unrelated to the lease. Off-lease, the reply was dropped (#7215, #6890), and the documented workaround was to run the WeCom-enabled backend single-replica.

This routes the reply over the Redis Stream relay the repository already runs for the same shape of problem, so the replica holding the socket sends it.

- At-most-once across replicas and restarts via a Redis claim keyed on the turn, with a TTL derived from the relay's `ReplayGrace` rather than a constant.
- Delivery runs on a bounded dispatcher, never on the shared shard read loop, with per-installation lines that keep two answers to the same person in order even across a re-offer.
- Attachments and `inbox:new` pushes are routed too, not just reply text.
- Every routed reply gets a final outcome, settled exactly once by the replica that routed it: `outbound_dropped_total{reason="no_live_connection"}` when no replica took the delivery. The residual window — every replica mid-reconnect — remains, but is now measurable instead of silent.

Multi-replica WeCom is supported in sharded/dual relay mode; legacy mode and deployments without Redis keep the previous behaviour and the single-replica requirement. `SELF_HOSTING.md` and the four `channels*.mdx` docs state the split.
2026-08-31 15:35:46 +08:00
f93855470f MUL-6874 fix(desktop): recover unexpectedly stopped daemon (#7808)
* MUL-6874 fix(desktop): recover unexpectedly stopped daemon

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

* MUL-6874 fix(desktop): bound daemon recovery retries

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

* MUL-6874 fix(desktop): queue one-shot daemon operations

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

* MUL-6874 fix(desktop): reject nested daemon operations

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-31 15:27:30 +08:00
mikyan fe59262188 feat(agent): add Huawei Cloud CodeArts support (#6985)
* feat(agent): add Huawei Cloud CodeArts support

* fix(migrations): resolve CodeArts prefix collision

* fix(agent): address CodeArts review feedback

* fix(agent): tighten CodeArts integration scope

* fix(agent): resolve CodeArts follow-up review blockers

* fix(agent): redact CodeArts launch arguments

* docs: sync supported runtime counts

* fix(agent): own CodeArts model probe process tree
2026-08-31 12:30:59 +08:00
Kinsoandjzli 9c8c7f039c feat(mobile): enable native iPad support (#7793)
Co-authored-by: jzli <[email protected]>
2026-08-31 12:28:14 +08:00
64ec7f5416 MUL-6737: fix(timeouts): close the two gaps the 2h inactivity budget left open (#7699)
* fix(timeouts): close the two gaps the 2h inactivity budget left open (MUL-6737)

Two independent places where a timeout still answered the wrong question.

Codex runs a semantic-inactivity timer inside the app-server protocol, and
unlike the daemon's watchdog it cannot see that a tool is in flight: a
commandExecution that prints nothing for the window trips it anyway. At a
fixed 10m ceiling that timer killed a quiet test suite or an
output-buffering `docker build` long before the 2h daemon budget meant to
protect it, which made "we raised the budget to 2h" false for Codex users.
It now derives from the daemon budget, taking the larger of the idle and
tool windows because it stands in for both. A tool budget of 0 means "never
force-stop during a tool", which this timer cannot express, so it falls back
to the idle budget rather than running unbounded; with the watchdog suite
disabled entirely Codex keeps its own built-in default, as it always has.
The env override and flag are unchanged.

Queued tasks expired on a pure wall clock: queued longer than a TTL meant
failed. That conflated "nobody is coming for this" with "the queue ahead is
long", and only the first is a failure. MUL-6558 was the second — a
self-hosted runtime with low concurrency held its own queue past 2h and
healthy work died as queued_expired. The TTL knob added then only moved the
cliff. Expiry now keys on whether the owning runtime still proves liveness,
the same signal FailTasksForOfflineRuntimes already uses for
dispatched/running rows, so a daemon going down retires its queued and its
in-flight work on one clock instead of two. A busy runtime keeps its
backlog for as long as it takes. MULTICA_TASK_QUEUED_TTL is removed with
the wall clock it configured.

Heartbeat age is read directly instead of gating on runtime.status='online'
so a row stuck at 'online' with a dead heartbeat still releases its queue,
and rows with no usable runtime binding — runtime_id IS NULL from before
migration 251, whose CHECK landed NOT VALID, or a runtime row that is gone —
are expired on sight rather than stranded forever by a JOIN that matches
nothing.

Tests: the queued sweep test now runs both phases against the same rows,
asserting the 5h-old task survives while its runtime heartbeats (the
MUL-6558 regression) and only expires once that runtime goes silent.
Assertions are scoped to the test's own task ids, because a runtime-keyed
sweep legitimately also collects queued rows other tests left on the shared
fixture runtime.

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

* fix(sweeper): give a queued task its own reconnect grace before expiring it

Review catch: keying expiry on runtime liveness alone was too aggressive in
one direction. Enqueue binds a task to agent.runtime_id without checking
that the runtime is up (task.go CreateAgentTask call sites), so for a
runtime that has already been dark for longer than the grace, the liveness
clause is satisfied the moment a new task is created — assigning an issue
to a laptop closed overnight would fail it inside one 30s sweep tick
instead of waiting for the machine to come back. That is the opposite of
what "reconnect grace" promises.

Require the row's own age as well: a queued task now waits a full grace,
counted from when it started waiting, before it can be given up on. A
heartbeating runtime still never expires its backlog, so the MUL-6558 fix
is unchanged. The age bound also stops the sweep re-evaluating the runtime
subquery against every queued row on every tick.

Also remove the deploy surfaces the previous commit missed — the knob was
deleted from the server but helm (values + configmap), the self-host
compose file, and the CI-run helm render assertions still advertised it, so
operators would have kept setting a variable nothing reads. Same for five
orphaned comment lines in .env.example describing the removed variable.

Correct the fail-closed comment on the unbound-runtime arms: they are not
reachable for "rows predating migration 251" — runtime_id was NOT NULL from
migration 004 until 251 replaced it with a CHECK that every insert and
update is verified against, and the migration-004 FK is ON DELETE RESTRICT.
The arms stay as defence against a future schema change, which is what the
comment now says.

Tests: a third phase covers the reported case — a task enqueued against an
already-dead runtime survives, then expires once it has waited a full grace
of its own. Phase 2's expectation is corrected accordingly: the row created
at now() no longer expires when its runtime dies, because it has not waited
its own grace yet.

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

* docs(timeouts): state both queued-expiry conditions and correct the FK claim

Review follow-up on b567511.

The user docs still described queued expiry as a single condition — "fails
once the runtime has been silent past the reconnect grace" — which is the
pre-b567511 behaviour and skips exactly the case that commit fixed. A
reader assigning work to a machine that went offline last night would
conclude the task already qualifies to fail. All four locales of tasks,
daemon-runtimes and troubleshooting now state both conditions and why the
second one exists.

The environment-variables pages contradicted themselves: the env table had
been updated to say the Codex semantic budget is derived, while the
`multica config` supported-keys table further down still advertised a 10m
default. That table now describes the resolution rule instead of a value.

The schema comment added in b567511 named the wrong constraint.
agent_task_queue_runtime_id_fkey (migration 004) is ON DELETE CASCADE; the
ON DELETE RESTRICT one is agent_runtime_id_fkey on agent.runtime_id. The
conclusion is unchanged — a queued row cannot reference a deleted runtime —
but the mechanism is a cascade plus migration 251's explicit unbind of
history rows, not a restrict.

Also log the resolved codex_semantic_inactivity at daemon startup, for the
same reason tool_watchdog is logged: it is now a derived value, so without
it the effective Codex budget cannot be read anywhere until a timeout
fires. And correct a test comment that still claimed both non-exempt rows
fail — the fresh row now stays queued, which is the assertion right below it.

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-28 18:00:35 +08:00
b25701b922 docs(changelog): add v0.4.36 release entry (2026-08-28) (MUL-6787) (#7697)
* docs(changelog): add v0.4.36 release entry (2026-08-28)

Add the 2026-08-28 changelog entry in en/zh/ja/ko and bump apps/web to 0.4.36.

MUL-6787

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

* docs(changelog): drop unreleased billing items from v0.4.36 entry

Cloud billing is still in staging and has never shipped, so the Issue count
limit and its upgrade path must not appear in the public changelog. Retitle
around the changes that are actually live.

MUL-6787

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-28 17:40:44 +08:00
1179142c13 MUL-6784: fix(desktop): share public web URLs (#7684)
* fix(desktop): share public web URLs

* fix(navigation): keep the current fragment in shareable URLs

Rebuilding the current page from `pathname` + `searchParams` alone dropped
the fragment, so feedback sent from a `#comment-…` deep link reported the
whole issue and the maintainer lost the position the reporter was at. Web
regressed too: it previously submitted `window.location.href`, fragment
included.

The fragment cannot be read from `window.location` on desktop — the
renderer is a MemoryRouter over the packaged `file://` page — so the
platform boundary has to carry it. `NavigationAdapter` now reports `hash`
alongside `pathname` / `searchParams`, and `currentPath()` composes the
three so no caller has to remember the fragment again.

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-28 16:46:44 +08:00
Bohan Jiang dcc2d58251 MUL-6737: fix(daemon): raise the agent inactivity budget to 2h and derive the tool budget (#7685)
The idle watchdog was sized to notice a hang quickly (30m), but the two costs it trades between are not symmetric: force-stopping a healthy run throws away the work and flips the issue to blocked, while noticing a hang late only holds a concurrency slot.

- DefaultAgentIdleWatchdog 30m -> 2h, sized by the longest legitimate silent step.
- AgentToolWatchdog now derives from the idle budget in both directions; MULTICA_AGENT_TOOL_WATCHDOG still overrides and 0 keeps its 'never force-stop during a tool call' meaning.
- Watchdog tick capped at 5m, so worst-case detection is budget + 5m instead of budget * 1.5.
- Dropped the unreachable 30s tick floor (window >= 1m implies window/2 >= 30s) and asserted the property it was written for directly.
- Removed the dead MULTICA_CODEX_TIMEOUT from .env.example; documented the generic watchdog and Codex timeout rows in CLI_AND_DAEMON.md; synced four doc locales.
2026-08-28 16:08:08 +08:00
acf6864f53 test(frontend): remove duplicate DOM coverage (#7631)
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-27 17:56:27 +08:00
loongerandJ 9a19c90f8e feat(skills): add Import from local to the New skill dialog (MUL-6703) (#7570)
Adds an "Import from local" route to the New skill dialog, so a member can pick
a folder containing SKILL.md or a .skill / .zip archive and import it straight
into the workspace. Shows name, description and file count before importing,
and fails clearly on a missing SKILL.md, oversized input or a name conflict
instead of creating a partial skill. Reuses the existing POST /api/skills/import
endpoint; web and desktop share the entry point. Docs and locales updated for
all four languages.

Directory selection is decided from path + size before any file is read, so a
large project folder cannot be pulled into memory. Concurrent selections are
guarded by a generation counter, and the import response is parsed through a
zod schema before its status is branched on.

Co-authored-by: J <[email protected]>
2026-08-27 11:19:51 +08:00
09a2410e88 refactor(agents): rename starter_prompts to conversation_starters (MUL-6709) (#7592)
* refactor(agents): rename starter_prompts to conversation_starters

The UI, docs and all four locales call this feature "Conversation starters"
(#7573), but every identifier behind it still said `starter_prompts` — the DB
column, the wire field, the `/api/config` capability flag, the TS types, the
i18n keys, and the `?focus=` token that a user can read in their own address
bar. The old name is also the one #7573 rejected on the way in: it promises a
prompt, while selecting a starter only prefills the composer.

Rename it everywhere so the identifier matches the product.

This is a breaking rename with no compatibility shim, and it is free exactly
once: the column arrived in migration 404 (#7261) after the last release tag,
so nothing has shipped. No deployed client reads the old name, no stored data
uses it, and `agent_conversation_starters_supported` has no version skew to
straddle. After this release, the same change would cost a wire alias, a
dual-written capability flag, and older Desktop/Mobile builds silently hiding
the editor.

Migration 404 stays as-shipped; 432 renames the column and its CHECK
constraint, so an already-migrated dev or staging database converges with a
fresh one.

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

* docs(skills): name the conversation-starters surface for agents

`multica-creating-agents` mentioned `conversation_starters` only as a wire
field in two tables. It never said what the feature is called in the product,
where a human configures it, or what selecting one does — so an agent reading
this skill would tell a user about "conversation starters" as a JSON field and
leave them with nowhere to go.

Add the product name (both languages), the Instructions-tab deep link, and the
behaviour that makes the name accurate: selecting one only fills the composer.
Also record that an agent with none configured still shows three built-in
defaults, so "the Chat shows suggestions" is not evidence this agent has any.

Document one gap the field table implied away: the accepted-body list is not a
CLI surface. `conversation_starters` has no `agent create` / `agent update`
flag — only `agent copy` carries an existing value forward — so an agent
following the old text would build a create command that silently drops it.

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

* fix(onboarding): keep the released starter_prompt field on the pre-v3 shim

The rename in ec44cc0ef swept up `starter_prompt` (singular) on
`bootstrapOnboardingRuntimeRequest`. That is a different contract from the
per-agent `starter_prompts` this PR renames: it is a field on the endpoint
marked "DEPRECATED, kept for desktop < v3", and unlike the per-agent one it
HAS shipped — it is in v0.4.34, released since fbd965e5bf.

Renaming it breaks the clients it exists for, and breaks them silently: the
handler decodes with a plain json.Decoder, so an old Desktop build's
`starter_prompt` would be dropped as an unknown field, the request would still
return 200, and the onboarding issue would quietly fall back to the default
description instead of the text the user typed. The test change hid it by
moving the fixture to the new name, removing the only coverage of the old one.

Revert both files. The two features share a word root and nothing else — the
free-rename argument for the per-agent field does not extend to a contract
that is already in the wild.

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-26 19:01:01 +08:00
BintengandBinteng f74a71060f perf(web): preload landing hero background (MUL-6711) (#7582)
Preload the above-the-fold landing background so it starts downloading alongside
the product preview instead of after it, shortening the black-fallback window on
cold loads. Also declares an explicit `sizes="100vw"` on the backdrop and moves
the product preview off the deprecated `priority` prop onto `preload`.

Co-authored-by: Binteng <[email protected]>
2026-08-26 18:41:01 +08:00
21b938bfdb docs(changelog): drop reverted entries from v0.4.35 (MUL-6709) (#7589)
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-26 18:13:36 +08:00
c7d66071e1 docs(changelog): add v0.4.35 release entry (2026-08-26) (MUL-6709) (#7583)
* docs(changelog): add v0.5.0 release entry (2026-08-26) (MUL-6709)

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

* docs(changelog): release as 0.4.35 instead of 0.5.0 (MUL-6709)

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-26 18:05:51 +08:00
LinYushen 3b1018e9c9 Revert "MUL-6016: feat(task): retry exact Codex capacity failures (#6747)" (#7586)
This reverts commit 8fda4f22d5.
2026-08-26 18:05:17 +08:00
26c04803c8 MUL-6698: feat(agents): make conversation starters discoverable from the chat they run in (#7573)
* feat(agents): make conversation starters discoverable from the chat they run in

"Suggested first actions" named its content but never its surface, and the
chat empty state it drives gave no sign those buttons were configuration at
all. The link between the two existed only in the head of whoever built it.

Close the gap from both ends rather than explaining it:

- Rename to "Conversation starters" and rewrite the hint to name the surface
  ("shown when someone opens a new chat with this agent"). "Quick action" was
  considered and rejected: it is already taken by chat follow-ups, the issue
  sidebar and settings, and it promises execution where selecting a starter
  only fills the composer.
- Add a "Customize starters" link to the empty state for anyone who may edit
  that agent, deep-linking to its Instructions tab with the editor scrolled
  into view and briefly ringed. Readers never see it.
- Preview the real empty state under the editor, rendered through the same
  component and resolver the chat uses so it cannot drift — and label the
  fallback explicitly, which is how an author learns the three suggestions
  they never configured are defaults they can replace.

The permission rule is one pure function shared by both chat surfaces: no
link for a reader, an archived agent, or a backend that drops starter
prompts (its Instructions tab renders no editor to land on).

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

* fix(agents): re-arm the starter-prompts deep link and sync localized docs

Two findings from review.

The focus latch was one-shot per mount, so it swallowed every click after
the first: a chat window left open beside the Instructions page could send
the same link again and nothing would happen — and the `focus` param would
be left stranded in the URL. It also blocked a link aimed at a DIFFERENT
agent when navigating between two agent pages on the same route. Re-arm the
latch when the param disappears and key it by agent id, keeping the guard
that stops a lagging URL update from looping. Both cases now have regression
tests, each verified to fail against the previous latch.

The zh/ja/ko copies of the agent docs still carried the old name and said
nothing about where the setting takes effect. Sync all six files with their
shipped UI translations and with the English additions (surface, preview,
Customize link).

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-26 15:34:01 +08:00
mrlonely 1ef1d65bef fix(cli): remove obsolete autopilot priority flag (#7557) 2026-08-26 14:40:44 +08:00
8fda4f22d5 MUL-6016: feat(task): retry exact Codex capacity failures (#6747)
* docs: record Codex capacity retry design

* docs: add Codex capacity retry implementation plan

* fix(taskfailure): recognize exact Codex capacity error

* feat(task): retry exact Codex capacity failures

* feat(server): configure Codex capacity retries

* docs: expose Codex capacity retry setting

* fix(task): align capacity retry comments

---------

Co-authored-by: cuijs <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-08-26 12:50:20 +08:00
Yuri GuiandAgent Op f8ec870f30 MUL-6629: feat(agents): add per-agent starter prompts for chat (#7261)
* feat(agents): add per-agent starter prompts

Co-authored-by: Agent Op <[email protected]>

* fix(agents): default omitted starter prompts

Co-authored-by: Agent Op <[email protected]>

* fix(migrations): renumber agent starter prompts

Co-authored-by: Agent Op <[email protected]>

* fix(chat): make starter prompt prefills reliable

Co-authored-by: Agent Op <[email protected]>

* fix(agents): preserve starter prompt writes

Keep dirty starter-prompt edits across agent refetches and fail closed when an older server cannot persist them. Clarify that copied starter prompts have no dedicated CLI override.

Co-authored-by: Agent Op <[email protected]>

* fix(migrations): avoid starter prompt prefix collision

Main now uses migration prefixes through 403, so move the starter-prompts migration to the next available prefix before requesting another review.

Co-authored-by: Agent Op <[email protected]>

* fix(agents): preserve edits after failed save

Ignore optimistic instruction snapshots while a save is pending so a rejected update can roll back the cache without erasing the user's submitted starter prompts. Cover the delayed optimistic-update and rollback sequence for both label and prompt fields.

Co-authored-by: Agent Op <[email protected]>

---------

Co-authored-by: Agent Op <[email protected]>
2026-08-25 22:43:16 +08:00
YYClaw 9e2d9b6d1c MUL-6661 feat(channels): add /new and /clear conversation controls (#7468)
* feat(channels): add new and clear conversation controls

* fix(dingtalk): resolve bot identity before dispatch

* feat(chat): expose channel-created chats consistently

* refactor(channels): snapshot task delivery routes
2026-08-25 18:28:01 +08:00
8cc9a879b8 docs(changelog): add v0.4.34 release entry (2026-08-25) (MUL-6670) (#7539)
* docs(changelog): add v0.4.34 release entry (2026-08-25) (MUL-6670)

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

* docs(changelog): rewrite v0.4.34 entry copy for clarity (MUL-6670)

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: J <[email protected]>
2026-08-25 17:35:24 +08:00
11ab949dc9 MUL-6611: docs(self-host): route /health through the proxy, add single-origin example and release-tag note (#7466)
* docs(self-host): route /health through the proxy, add single-origin example and release-tag note

Document the four traps we hit deploying twice from the quickstart alone,
across all four doc locales (en/zh/ja/ko):

- `multica setup` probes <server-url>/health and needs a 200, but the web
  image does not proxy /health, so a proxy that forwards everything to the
  frontend makes setup fail with "Server not reachable" against a healthy
  stack. Add the explicit route to both Caddy examples, a new
  troubleshooting section, and a hint at the CLI setup step.
- Add a single-origin reverse-proxy variant (one domain / one port), the
  shape small servers actually use, with the daemon-critical paths called
  out (/health, /ws).
- The published images track the newest release tag while a plain clone
  checks out main. Anyone building the CLI or images from the checkout
  should pin the tag first or binaries drift from the containers.
- Add an /api/config verification step: daemon_server_url must print the
  public URL, and on Docker Compose < 2.24 FRONTEND_ORIGIN/MULTICA_APP_URL
  do not follow FRONTEND_PORT (the ${FRONTEND_PORT} references copied from
  .env.example stay literal, refs #6146).
- Note that a curl against /ws returning 400 is expected without a daemon
  token; the backend log entry proves the proxy chain works.

* docs(self-host): review fixes — WS 400 attribution, section placement, grep over jq

- The WS-400 note attributed the rejection to daemon-token auth; the actual
  reason a bare curl gets 400 is the missing workspace query parameters
  (browsers auth by cookie, daemons auth in the first frame after upgrade).
  Fixed in all four locales.
- The new troubleshooting section was inserted before the trailing two
  paragraphs of the realtime section, swallowing them; moved it after the
  reverse-proxy example line.
- daemon_server_url comes from MULTICA_PUBLIC_URL when set (otherwise
  MULTICA_APP_URL) and is omitted when both are unset — the verification
  note now says so, and the command uses grep instead of jq (jq is not in
  the prerequisites list).
- zh: 问题排查 → 故障排查 to match the page title; ja: replaced —— with
  the file's established " — " / parenthetical style; linked the matching
  row in the Common issues tables to the new anchor in all locales.

* docs(self-host): route /api/daemon/ws in the single-origin example

Daemons dial {server-url}/api/daemon/ws for their long connection, not
/ws — and the web image cannot proxy WebSocket upgrades (which is why
/ws is routed to the backend directly in every example). The single-
origin variant added earlier only routed /ws, leaving the daemon's
handshake to fail through the frontend; the daemon then silently falls
back to polling, with repeated status=400 lines on that path in the
backend log as the only symptom. Add the explicit handle to the
single-origin example in all four locales, and a self-host note in the
troubleshooting WebSocket section.

* docs(self-host): address review — drop Compose version claim, qualify /health wording, bound the curl-400 check

- Remove the unverified 'Docker Compose < 2.24' cutoff; advise setting
  FRONTEND_ORIGIN/MULTICA_APP_URL explicitly when /api/config prints
  localhost.
- Qualify /health statements for merged #7467: current web releases
  forward the path, older ones do not; keep the explicit direct route
  (works across releases) and list DNS/TLS/firewall/timeout/5xx as
  other probe failures. Covers the Caddy comment, the setup section,
  the FAQ row, and the troubleshooting page in en/zh/ja/ko.
- A bare curl 400 on /ws only proves plain HTTP routing reaches the
  backend (workspace-param rejection happens before the upgrader);
  point at DevTools Network→WS for a real 101 handshake check.
- ja: 転達 → 転送; fix the stale 'Step 1' self-link anchors (en/zh/ja/ko).

* docs(self-host): document the real daemon_server_url precedence and omission

daemonSetupURLsFromEnv resolves the app URL as MULTICA_APP_URL falling
back to FRONTEND_ORIGIN, and omits both daemon fields from /api/config
when that chain is empty — even if MULTICA_PUBLIC_URL is set. The docs
said 'MULTICA_PUBLIC_URL, otherwise MULTICA_APP_URL; empty means
neither is set', which missed the fallback and mis-stated the omission
condition. State all three inputs and the actual condition in
en/zh/ja/ko.

* docs(self-host): include MULTICA_DAEMON_SERVER_URL in the daemon_server_url precedence

daemonSetupURLsFromEnv reads MULTICA_DAEMON_SERVER_URL before
MULTICA_PUBLIC_URL (server/internal/handler/config.go:128), so the
verification note in all four locales was one step short of the real
chain. environment-variables.mdx already documents it in full.

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-25 13:07:20 +08:00
da7131ab59 MUL-6625: enforce prepaid member seat capacity (#7491)
* MUL-6625: enforce prepaid member seat capacity

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

* fix(seat-capacity): address review findings

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

* fix(seat-capacity): close retry and worker races

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

* fix(seat-capacity): bound lock waits during rollback

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

* fix(seat-capacity): pin the cross-repo confirm settlement budget

Multica Cloud must wait longer than this worker's whole retry budget before it
treats a member row as one its ledger never recorded; adopting a member whose
confirm is still retrying counts one person as two seats, and an over-estimate
is what asks a customer to buy seats they do not owe.

That budget was only implicit here — 10 attempts of exponential backoff plus a
tick and a lock wait each — and Cloud's horizon had been set from a single
retry. Extract the backoff into retryBackoff, expose SettlementBudget, and
assert the worst case (29m15s) stays inside Cloud's 45m horizon, so raising
maxAttempts or the backoff cap fails review here instead of widening the window
in which Cloud cannot quantify a ledger.

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

* Revert "fix(seat-capacity): pin the cross-repo confirm settlement budget"

This reverts commit d0ec36f647.

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-25 12:51:44 +08:00
1acdc68e58 docs(changelog): add v0.4.33 release entry (2026-08-24) (MUL-6642) (#7502)
* docs(changelog): add v0.5.0 release entry (2026-08-24) (MUL-6642)

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

* docs(changelog): omit gated WIP release notes (MUL-6642)

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

* docs(changelog): retitle release entry and use 0.4.33 (MUL-6642)

Replace the vague "local environments" title item — it named a
contributor-only `make up` change with no user-visible surface — with the
localized task failure messages users actually see, and drop that feature
line from the entry.

Keep the release on the 0.4.x line as a patch bump over v0.4.32.

Also restore the ja/ko indentation the previous commit shifted, so both
files diff as pure additions again.

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: J <[email protected]>
2026-08-24 17:51:02 +08:00
mrlonely c8154f9d1a feat(selfhost): add daemon server URL override (#7331) 2026-08-24 15:13:36 +08:00
8a5a6adbf7 feat(agent): add ZeroClaw as a native ACP runtime (MUL-6511) (#7351)
* feat(agent): add ZeroClaw as a generic ACP agent runtime

Add ZeroClaw (a Rust-based, single-binary generic agent CLI) as a
first-party runtime, driven over the standard ACP JSON-RPC transport via
`zeroclaw acp`, mirroring the Dim/QwenPaw/Grok ACP backend shape. Only the
binary, launch args, and blocked-flags policy are ZeroClaw-specific; the
backend reuses the shared hermesClient ACP transport and deliverable
tracker.

No provider-specific quirks (permission-preset injection, minimum-version
gate, or a separate authenticate step) are assumed without evidence — those
were verified against real Dim/Grok binaries and would need the same
hands-on verification against a real ZeroClaw binary before being added
here.

Backend: SupportedTypes/New/launchHeaders wiring, model discovery via
session/new's advertised catalog, daemon probe with
MULTICA_ZEROCLAW_PATH/MODEL overrides, AGENTS.md context delivery,
default-command-name registration, metrics label, and a migration widening
the runtime_profile.protocol_family whitelist.

Frontend: provider union, MCP-config support flag, and a placeholder
provider-logo mark (no official ZeroClaw brand asset exists yet).

Tests mirror the existing ACP backend conventions: fresh-session,
resume-via-session/load, resume-not-found, transient-load-error, blocked
launch args, timeout, and the shared ACP deliverable-boundary regression
suite.

* fix(agent): renumber ZeroClaw's migration to 378 to avoid a same-day collision

Both this branch and a concurrently-implemented Junie runtime branch
independently picked migration 377 off the same origin/main tip.
Renumbering ZeroClaw to 378 up front avoids opening two PRs from the
same author that claim the same migration number.

* fix(agent): align the ZeroClaw ACP backend with the real 0.8.4 runtime

The backend was written against an assumed vanilla ACP handshake and never
checked against a ZeroClaw binary. Driving a real zeroclaw 0.8.4 over stdio
shows five of those assumptions are wrong, and the test fake encoded the same
assumptions, so the suite stayed green against a contract the runtime does not
have.

- Drop the session/set_model request. It is not in ZeroClaw's dispatch table
  (-32601) and no ACP handler reads a model param at all, so it could never
  succeed; because its failure was fatal, any agent with a model configured
  failed before session/prompt was reached. The model comes from the ZeroClaw
  agent profile, so opt zeroclaw out of ModelSelectionSupported and stop
  reading MULTICA_ZEROCLAW_MODEL rather than leaving the UI offering an
  override that is silently ignored.

- Label token usage from initialize's _meta.zeroclaw.defaultModel, the only
  model identity the ACP surface exposes. session/new returns just
  {sessionId, workspaceDir}, so the two extractACPCurrentModelID reads were
  dead code and the model-catalog discovery they fed could only ever return an
  empty fallback while paying an ACP subprocess per catalog read. Return an
  empty catalog directly, as qwenpaw/mcode do.

- Resume through session/resume instead of session/load. Both restore the
  transcript into the agent, but load also replays every retained message back
  as session/update notifications, which fed the previous answer into this
  turn's deliverable. Add the acceptNotification turn gate every other ACP
  backend carries, so out-of-turn chunks are dropped even if a runtime flushes
  them early (#1997).

- Send agentAlias on session/new when the operator names one. ZeroClaw
  auto-selects an agent only when its config holds exactly one entry, so
  multi-agent installs failed with -32602 and had no way out: `zeroclaw acp`
  has no --agent flag, and passing one kills the process at argument parsing.
  The alias is consumed from custom_args and travels as a param; it is omitted
  when unset, because a hardcoded guess breaks the single-agent case that
  works today. The refusal now names both remedies.

- Stop advertising MCP support. No ZeroClaw handler reads params.mcpServers;
  acp_enable_mcp gates that agent's own mcp_bundles, not the client list, so
  MCP is operator-side only. Hide the MCP tab and warn on a stale mcp_config
  instead of failing the task.

Also teach isACPSessionNotFound about ZeroClaw's custom -32000
SESSION_NOT_FOUND, without which a stale session id was retried forever
instead of restarting fresh. The wording check still decides, so a transient
error under the same code stays non-rejecting.

Tests now encode the observed handshake — _meta default model, session/new
without a catalog, -32000 for an unknown session, -32601 for set_model — and
cover the replayed-history, alias and MCP paths.

* fix(agent): make ZeroClaw integration merge-safe

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

* fix(agent): tighten ZeroClaw ACP fallbacks

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

* fix(agent): preserve ZeroClaw retry semantics

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-24 14:39:08 +08:00
Sebastian San Blas 701eaec454 fix(mobile): use flat app icon (#7453) 2026-08-24 13:50:29 +08:00
Felix021 fbb4ec59da MUL-6610: fix(web): proxy /health to the backend in the runtime rewrite (#7467)
* fix(web): proxy /health to the backend in the runtime rewrite

`multica setup self-host` probes {server-url}/health and treats any
non-200 as "Server not reachable". The backend serves the path, but the
web image's runtime rewrite only covered /docs, /v1, /api, /uploads, /ws
and /auth, so a same-origin reverse proxy that forwards everything to the
web image left the probe 404ing at the Next.js router — setup failed
against a healthy stack unless the proxy also routed /health to the
backend explicitly. Add the exact path next to /ws, in both the runtime
rewrite and the next.config.ts dev rewrites.

* test(web): cover /health at the proxy layer, guard /healthz and /readyz
2026-08-24 13:15:22 +08:00
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