Skip to content

chore: upgrade fork to latest upstream Multica - #9

Merged
harley merged 124 commits into
mainfrom
chore/upstream-upgrade-20260924
Sep 24, 2026
Merged

harley merged 124 commits into
mainfrom
chore/upstream-upgrade-20260924

Conversation

@harley

@harley harley commented Sep 24, 2026 •

Copy link
Copy Markdown

What does this PR do?

Update the CoderPush fork to upstream e909e9c89d524cd54c1d5f4fa5963882093efdc9 and prepare immutable backend/web images for the Singapore deployment. Preserve the fork's duplicate-assignee guard and completion instructions while retaining upstream cancellation handling.

Related Issue

User-requested production upgrade from v0.4.43 plus handoff backport.

Type of Change

  • CI / infrastructure
  • Bug fix
  • Documentation update

Changes Made

  • Merge 122 upstream commits since the shared baseline.
  • Resolve the child-completion conflict by retaining cancellation warnings and acceptance-based parent completion.
  • Add manual CoderPush main image builds and digest-based Compose overrides.
  • Preserve the production operating runbook.

How to Test

  • Fresh isolated database migrations through 547 completed locally.
  • Focused DB-backed handoff, child-completion, and cancellation race tests passed (verbose output confirms execution). Full local handler suite passed. Local full Go run hit the upstream Dsh probe timeout on macOS; Linux CI passed in full; the macOS Dsh probe passed in isolation.
  • Focused integration review and Amazon Q review found no concrete blockers.
  • Production backup restored in an isolated, internet-blocked rehearsal container. All 77 migrations from schema 467 to 547 passed; 6 workspaces, 106 issues, and 46 agents preserved. New backend health/readiness passed. Rehearsal schedules disabled and no production integration keys supplied.
  • Authenticated production UI baseline screenshot saved locally; after-upgrade UI verification follows cutover.

Risks

Production moves from schema 467 through 77 migrations. Migration 468 deletes obsolete link rows and drops columns. An image-only rollback is insufficient; retain and rehearse a database restore. Keep workers and backend stopped during the final backup and migration. Preserve production secrets, signup domain policy, ports, uploads, and runtime configuration.

AI Disclosure

AI tool used: Codex.

Prompt / approach: Inspect live production, merge upstream in an isolated worktree, preserve custom behavior, verify migrations and tests, and deploy with backups and rollback evidence.

ii-jj and others added 30 commits September 17, 2026 12:37
…ica-ai#7980)

* fix(wecom): show the agent the message a sender quoted

An aibot callback carries the content of the message the sender replied
to, in `quote`. The adapter's aibotMsgCallback never declared the field,
so it was dropped at the JSON boundary.

That makes a reply unanswerable. "这个怎么处理" quoting an alert is a
complete question in the chat and an empty one to the agent, which gets
the four words and none of what they point at. The sender sees a bot that
ignored what they were obviously asking about.

Rendered as a labelled blockquote ahead of the sender's own words, using
the same [Image]/[File]/[Video] vocabulary the media placeholders already
use so an agent reading every channel through one prompt meets one
spelling. A quoted 图文混排 nests one level deeper than a mixed run, which
is the only reason quotedMessage exists rather than reusing mixedItem
outright.

Deliberately NOT in ownCommandSource: the command parsers read the first
non-empty line, and a quoted line is not one the sender typed here.
Prefixing it would let a quote of somebody else's "/issue …" file an
issue nobody asked for — covered by a test.

WeCom sends no msgid and no author userid alongside the quoted content,
so it can only be rendered, never resolved back to a stored message.

* fix(wecom): strip a bare directive that arrives behind a quote

Review found the case the first commit missed: /new and /clear with no body
of their own, sent as a reply.

normalizeWeComControlLayout declines a directive with an empty body and no
media on purpose — that is the shared pending sentinel, and consuming it
would open a session with an empty first turn. A quote breaks the premise
the gate rests on. "/new" replying to an alert is not an empty message; the
alert is the whole of what the person sent. The gate could not see the
quote, so the directive stayed in the body — and once rendered behind
"> [Quote] …", Router could no longer strip it either: both of its routes
(re-parsing Text, and Text == CommandText) are defeated by the prefix.

/new was persisted as the first turn of the session it had just opened, and
inherited as context by every later turn. /clear failed the other way: the
FreshSession branch rewrote Text to the empty command body, so the quote —
the only thing the person sent — disappeared without a word.

A quote is now content, the same way media already is: it is the reason the
directive-only layout is not the sentinel. hasMedia becomes hasOtherContent
and the consumed-directive bookkeeping moves after the quote is prepended,
so CommandText names the body that is actually left rather than reaching
the same place through Router's empty-CommandText fallback.

Router needed one change of its own, and no adapter could have made it:
persistMessage read CommandText and media only, so a turn whose entire
content is a quote looked empty. Text is now consulted too — by that point
it is "" for a genuinely bare directive, which keeps the sentinel intact.

Also bounds the quoted block at 500 runes. The sender quoted a document to
point at it, not to resend it, and their own words follow the block.

Pinned from both sides, because neither alone can see the defect:
wecom/regression_quoted_directive_test.go proves the adapter hands over a
stripped body, engine/regression_quoted_body_swallows_directive_test.go
proves Router keeps it. Both files also pin the sentinel — a bare directive
with nothing else must still pass through — so they fail on over-stripping
as well as under-stripping. Verified by reverting each change in turn: the
adapter revert fails the two adapter cases, the Router revert fails the
/new case, and the sentinel tests pass throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xUxTgkvJSCUpBTJDWZTYF

* fix(wecom): mark a quote as sender-selected context

The rebase landed on main's HasSelectedContext (message.go:149), which says
what this branch's Router change was reaching for and says it better: Text
may carry context the sender chose rather than typed, and such context is
input even when a control command has no body of its own. Router now derives
persistStartedMessage and bareFresh from it, and DingTalk, Lark and Telegram
already set it.

So the Router edit is gone and the adapter sets the field: a WeCom quote is
context the sender picked by replying to it. Behaviour is what review
verified — a bare directive behind a quote is a turn whose prompt is the
quoted message — but the rule is now main's, applied to one more adapter,
instead of a second meaning growing on this branch's empty-Text check.

That also answers the question review left open before merge. main decides
that a selected quote is a turn; a WeCom quote is a selected quote. If that
decision is revisited, it moves in one place for every channel rather than
in each adapter.

Pinned on both sides: the adapter test asserts the flag is set behind a
quote and unset for a bare directive with nothing else, and the Router
regression sends it. Hardcoding it false fails
TestABareNewBehindAQuoteLeavesOnlyTheQuote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wv2qRKBY6fCBnhVae7Mj58

* fix(wecom): give Router a command source for a quoted screenshot

Review found the case the branch owns without having introduced it: this
makes WeCom the third adapter that enriches Text, and the first that does not
satisfy the invariant at engine/router.go:200-208.

Quote a message, then reply with only a screenshot — an ordinary shape in a
room, the quoted-screenshot case seen from the sender's side. ownCommandSource
answers "" for a standalone photo on purpose: a placeholder is not words
anybody typed. quotedContext then prepends the quote, and Router's fallback
assigns the ALREADY enriched Text, so the Chat is named after the message the
sender replied to: "[Quote] 生产库连接数打满了". Verified end to end through
chatTitleSource / deriveFirstMessageTitle before fixing.

The body as it stood before enrichment is the honest command source, which is
lark's shape (ws_frame_decoder.go:113). It is still only what the sender sent,
and the placeholder in it is dropped downstream by deriveFirstMessageTitle —
so the title lands back on the media path the same screenshot takes with no
quote, which the engine test asserts by comparing the two.

Only fills an EMPTY command source, so a screenshot with "/issue 登录坏了"
under it still files that issue, and the bare-/clear assignment above keeps
precedence.

Pinned from both sides, as everything else on this branch: the adapter test
fails with "CommandText is empty behind an enriched Text", the engine test
compares the quoted and unquoted titles and refuses to pass if the fallback
it guards against stops leaking.

Known and left as is: a bare /new or /clear behind a quote still titles the
Chat "[Quote] …", because there the quote genuinely is all the person sent.
Stripping the label belongs in title derivation, not here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wv2qRKBY6fCBnhVae7Mj58

---------

Co-authored-by: YeJing <yejing@wisefin.tech>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…son guards (multica-ai#8340)

* fix(desktop): do not prepend PATH fallbacks over login-shell Node

Prepending /usr/local/bin after fix-path let a stale system Node 12
shadow nvm/fnm, breaking shebang CLIs (CodeBuddy/OpenClaw) on --version.
Append only missing fallback dirs so recovered shell PATH keeps precedence.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): align CodeBuddy stream-json guards with Claude

Pass stderr into resume rejection, fail on prompt_too_long terminal_reason,
and force foreground when Bash requests run_in_background — the same
failure modes Claude already handles for Multica-managed headless runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): align CodeBuddy guards with real CLI protocol

Address review on multica-ai#8340: force CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS,
fail on system/task_* background events, and drop Claude-only
prompt_too_long / async_launched handling that CodeBuddy 2.150.0 never emits.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): update CodeBuddy handleUser test after signature change

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): force CodeBuddy background-disable env last for Windows

Append CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS=1 so os/exec's
case-insensitive last-wins dedup keeps it over lowercase custom_env.
Also extract Desktop PATH fallback helper with unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): render canonical settled assistant answers

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): preserve task message arrival order

Co-authored-by: multica-agent <github@multica.ai>

* fix(chat): retain settled process narration

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
…not fill the host disk (multica-ai#8510)

* fix(agent): require OpenCode >= 1.1.54 so old builds cannot fill the host disk

OpenCode <= 1.1.53 ignores TMPDIR, TMP and TEMP when its embedded Bun runtime
extracts a native module, writing into the shared system temp directory
whatever the daemon exports. Each successful run leaves one 4-8 MB module
behind under a fresh, non-content-addressed name, so nothing ever reuses or
overwrites it. Upstream fixed this in 1.1.54 (2026-02-10).

Reproduced on Linux against pinned builds, verifying each binary's
self-reported version: 1.1.49, 1.1.52 and 1.1.53 write into the shared /tmp
with the three variables pointed elsewhere; 1.1.54 and everything after honour
them. multica-ai#8392 is what that costs a self-hosted daemon left on a pre-fix build —
~2,960 files, 11.16 GiB, root filesystem at 99%, and no error anywhere until
the disk runs out.

The per-task temp directory cannot contain a CLI that never reads the
variables, and deleting by filename in a shared /tmp is not safe — a module
another live process still has dlopen'ed looks identical. Refusing the CLI at
registration is the only place this can be stopped, and it turns a silent host
outage into "opencode version X is below minimum required 1.1.54 — please
upgrade".

Note this floor is unlike the others in the table: every existing entry names a
protocol the backend speaks through, so an older CLI cannot serve a task at
all. A pre-1.1.54 opencode serves tasks correctly; it damages the host while
doing it. The table comment now says which kind each entry is.

Refs multica-ai#8392

Co-authored-by: multica-agent <github@multica.ai>

* docs: list the OpenCode minimum runtime version

The install-agent-runtime callout enumerates the per-runtime floors the daemon
enforces at registration; OpenCode now has one, so it belongs in the list.
Updated in all four translations.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <agent@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
…s environment-independent (multica-ai#8512)

Four tests in this package failed for every agent running the suite from
inside a Multica task, and passed in CI:

  TestHermesMemoryStorePathLayout
  TestPruneHermesMemoryStores
  TestHermesSessionStorePathLayout
  TestPruneHermesSessionStores

They isolate themselves by pointing HOME at a t.TempDir() and then assert a
path under $HOME/.multica. But HermesMemoryStorePath / HermesSessionStorePath
resolve through cli.ProfileDir, and multicaConfigRoot consults
MULTICA_TASK_CONFIG_ROOT before it ever reaches HOME — returning that root
directly, without the .multica segment. The daemon sets that variable for
every task it runs (taskMulticaEnvironment), so overriding HOME had no effect
and the tests were asserting against a branch they never took. CI never sets
the variable, so it stayed green and the trap only ever hit agents.

Clearing the variable once in TestMain makes the package resolve profile dirs
the same way in both environments. Preferred over a per-test
t.Setenv(cli.TaskConfigRootEnv, "") for three reasons:

  - It also covers TestPruneHermesMemoryStoresDisabled and
    TestPruneHermesSessionStoresDisabled, which passed for the wrong reason:
    their stores were never created under the root the pruner scans, and the
    assertions held only because retention <= 0 disables the pruner outright.
  - t.Setenv cannot be called from a parallel test, so the per-test fix would
    force future tests that resolve a profile dir to choose between running in
    parallel and running in the right environment.
  - New tests in the package are correct by default rather than by remembering
    to copy a line.

A test that wants the task-local branch still sets the variable itself.

Verified on the same commit, whole package:

  go test ./internal/daemon/execenv/ -count=1                        ok
  env -u MULTICA_TASK_CONFIG_ROOT go test ./internal/daemon/execenv/  ok
  go test ./internal/daemon/execenv/ -count=1 -race                  ok
  go vet ./internal/daemon/execenv/                                  ok

Co-authored-by: multica-agent <github@multica.ai>
…ared profile (multica-ai#8483)

Deleting a profile-backed runtime instance is refused while its profile exists,
and the refusal said "delete its runtime profile instead". For the case that
produces it most often — a retired machine's leftover row in a profile other,
healthy machines still use — that points at a workspace-wide delete which takes
those machines' runtimes too, and which a bound agent refuses anyway with a
second message naming no agent and no machine.

Retention GC already deletes these rows after 7 days offline, ignoring
profile_id, so the refusals now describe that instead of recommending the
destructive route:

- Instance refusal is status-aware, and only promises automatic cleanup when it
  matches the gate gcRuntime actually applies: the runtime's own undrained
  tasks plus those owned by every user agent bound to it, archived included.
- Profile refusal names the blocking agents and the machine each sits on, with
  a per-class remedy — ordinary agents can be reassigned or archived; Mika
  cannot be archived but can be rebound; a Builder session is reachable only by
  its creator. Those clauses come from counts over the whole blocker set, so a
  class outside the 20-row sample is still reported.
- Response bounded at 20 entries with an exact total, capping rows read inside
  the locked delete transaction.
- CLI surfaces the server's sentence instead of dumping the raw JSON body.

When a delete is allowed or refused is unchanged; the guard predicates and the
instance-refusal error code are untouched. The remaining capability gap —
retiring an offline instance on demand — is tracked separately.

Fixes MUL-7409
…multica-ai#7623)

`multica runtime profile set-path` was accepted on Windows but the daemon never
used the override: the gate required a Unix executable bit, which Go never sets
on Windows files (0666/0444 for regular files; 0111 only on directories). Every
override — including a plain .exe — read as not executable, so the daemon fell
back to PATH: either failing registration with a message that blamed the user's
filesystem, or silently registering a different install than the pinned one.

appendProfileRuntimes now resolves the override through resolveAgentExecutablePath
(exec.LookPath) — the same contract agent launches use — and records the resolved
path rather than the raw override, so Windows PATHEXT completion picks up .cmd
shims and extension-less pins. Unix exec-bit checks are unchanged, and a stale or
mistyped override still falls back to PATH with the existing warning.

Regression coverage runs on the real Windows runner via the existing
windows-execenv job.

Fixes multica-ai#7613
…ultica-ai#8516)

The integrations directory derived each channel's "Connected" badge from
`installations.length > 0`. That read is correct for GitHub and VCS, which
hard-delete their row on uninstall, but wrong for the five IM channels:
revoking a bot flips `status` to 'revoked' and KEEPS the row for audit, so the
count can never fall back to zero. A torn-down Feishu bot kept showing a green
"Connected" on the Channels card while its own detail row correctly rendered
已撤销 — same payload, same cache, two different answers, and refreshing never
converged because it was a derivation bug, not staleness.

Read `status === "active"` through a shared `hasActiveInstallation` helper,
matching what every other consumer of that payload already does. Unknown or
missing states fail closed to "not connected", consistent with the existing
schema decision in packages/core/api/schemas.ts. GitHub and VCS keep their
count-based reads.

Refs multica-ai#8496 — that report bundles three defects and this fixes one; inbound
delivery and external-revocation detection remain open.
Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
…not only after a half-width space (multica-ai#8502)

The mention picker inherited Tiptap's default `allowedPrefixes: [" "]`, so an `@` only opened it after a half-width space or at the start of the text. CJK is typed with no separator (or with a full-width U+3000 from the IME), which left the picker unreachable.

The boundary rule now lives in `packages/core/markdown/mention-boundary.ts` and is shared by the web/desktop editor and the mobile composer: an `@` glued to a Unicode word character stays inert (`user@example.com`, `josé@example.com`, `почта@mail.ru`), while scripts written without word spaces open it (`你好@Mi`, `こんにちは@Mi`, `안녕하세요@Mi`). `用户@example.com` opening is a documented, accepted ambiguity. The MUL-5429 typed-`@` provenance check is unchanged, so a pasted `@` still does not open the picker.
)

The cmd/multica tests redirect os.Stdout into an os.Pipe and read it only
once the command has returned. Nothing drains the read end in between, so
a command can print no more than the pipe will buffer before its write
blocks and never completes.

On Linux that buffer is 64KB and nothing here reaches it. On macOS a pipe
with no reader stops accepting writes after 512 bytes, so any test whose
command prints a sizeable JSON payload deadlocks outright:
go test ./cmd/multica/ runs until the binary's timeout instead of
finishing, and -run on a single such test reproduces it on its own.

Start the reader before running the command, in every place that captures
a pipe this way. captureStderr already did this; the shape just had not
reached the stdout side or the copies inlined into individual tests.

The package went from never completing inside a 500s timeout to passing
in under two seconds.
* fix(daemon): durably replay terminal reports

Co-authored-by: multica-agent <github@multica.ai>

* fix(daemon): harden terminal report replay

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
…ogress (multica-ai#8495)

* fix: distinguish cancelled child work from done in stage progress

* test: align cancelled stage batch expectation

* fix: preserve named-stage cancellation warning in batch

* test: cover historical cancellation in batch-closed stage

* style: restore trailing newline in batch test
…k attribution (multica-ai#8412)

* fix(dingtalk): preserve answer context in quoted replies

* fix(dingtalk): narrow quoted reply handling to core behavior

* fix(dingtalk): apply conservative quote policy to card nodes

Apply dingTalkReadableQuotedText to raw card TEXT and LINK values. Mark
filtered nodes unavailable while preserving neighboring text, links and
image placeholders. Update captured-snapshot expectations and cover
node-local degradation.

Follow the conservative policy accepted by Bohan-J in PR multica-ai#8061: withhold
legitimate quoted content containing || rather than pass an unrecognized
opaque envelope to the agent as the sender's words. This is a projection
policy, not a vendor-defined opaque-content detector.

Review decision:
multica-ai#8061 (review)
Original concern about heuristic false negatives:
multica-ai#8061 (review)
Opaque sample:
open-dingtalk/dingtalk-stream-sdk-go#22

Validation: DingTalk package race tests, callback ACK regression, and
git diff --check passed. Updated card regressions failed before the fix.
* fix(agent): bound silent Pi provider failures

Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): preserve Pi errors across cancellation

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
…i#8489)

* fix(comments): explain @ALL member-only behavior

* fix(comments): clarify @ALL broadcast semantics

Do not promise notification delivery for edits or zero-recipient drafts.

Keep the preview tied to the structured @ALL token and cover both states.

---------

Co-authored-by: 高春晖 <gaoch@lianlianpay.com>
…ultica-ai#8321)

Adds "Project status" as its own issue filter dimension, next to "Project":
pick "In Progress" once instead of ticking every active project, and the
result follows project lifecycle changes instead of freezing a project-id
snapshot.

Server side extends the existing issue table filter protocol with an
optional, validated `project_statuses`, filtered through a parameterized,
workspace-bound EXISTS on `project`. No migration and no new index — the
predicate resolves through `project_pkey`, and the planner falls back to the
existing `idx_issue_project_status`.

Client side wires it as a normal dimension: filter menu, chip, reset,
saved views and Gantt/Swimlane, with the project catalog treated as
"cannot answer yet" rather than "no filter" while it loads. Project
status changes, deletes and realtime project events invalidate the issue
table cache, since issue rows themselves do not change.

Default off; closing the filter reverts the behavior.
Co-authored-by: multica-agent <github@multica.ai>
…lure (multica-ai#7805)

* fix: fail private runtime owner mismatches (PUCK-89)

Co-authored-by: multica-agent <github@multica.ai>

* fix: keep runtime mismatch repair off idle claim path

* test: cover mixed runtime claim outcomes (PUCK-89)

Co-authored-by: multica-agent <github@multica.ai>

* fix: settle runtime owner mismatches during claim

* fix: reject ownerless agents on private runtime claims

* chore: restore claim version sampling layout (PUCK-89)

Co-authored-by: multica-agent <github@multica.ai>

* fix: re-authorize delivery at the finalize boundary, dedicated runtime_access_denied client copy, fixture convention (PUCK-89)

Blocker 1 — final delivery gate: FinalizeTaskClaim now runs a caller-
supplied authorize closure inside its transaction. The singular and
batch claim paths share finalizeClaimDelivery, which re-locks the
runtime row (FOR UPDATE), re-reads the agent, and re-verifies the
private-runtime owner fence against CURRENT ownership before the task
token commits. A concurrent re-registration that would change
owner_id blocks until the gate commits, closing the stale-snapshot
TOCTOU. Mismatched tasks settle through the existing
failClaimedTaskBeforeLaunch -> FailTask path; singular keeps
200 {"task":null}, batch skips the task and keeps returning valid
ones. Regressions cover both paths.

Blocker 2 — client copy: runtime_access_denied gets dedicated
actionable copy (make runtime public / rebind-copy agent) on the
issues blocked-trigger mapping, both chat send-failure toasts, the
chat failure-reason map, the mobile dispatch-reason helper, and the
autopilot run-now toast. Generic fallbacks unchanged; locale keys
added for en/ko/ja/zh-Hans.

Blocker 3 — fixture convention: runtime_access_denied_test.go now
uses testutil.Call; daemon_runtime_access_test.go seeds its chat task
via the dbfx.Task fixture. Test semantics unchanged.

Co-authored-by: multica-agent <github@multica.ai>

* fix: bind final task token to locked runtime owner

Co-authored-by: multica-agent <github@multica.ai>

* fix: keep delivered user context on current owner

Co-authored-by: multica-agent <github@multica.ai>

* test: use testutil.Call in settlement-failure claim regression

Convert the last manual httptest.NewRecorder flow in
TestFinalizeClaimDelivery_SettlementFailureIsUnsettled to the repository's
required testutil.Call helper (hard convention from review) and drop the
now-unused net/http/httptest import. No production code changes.

Co-authored-by: multica-agent <github@multica.ai>

* PUCK-132: settle queued private-runtime owner mismatches as runtime_access_denied

Align persisted task failure semantics with admission semantics: queued
private-runtime ownership mismatches now settle as runtime_access_denied,
letting clients reach the dedicated recovery copy instead of the generic
invalid_task_identity copy. Actual task/agent identity violations
(agent rebound, agent deleted, response identity mismatch,
error_agent_runtime_changed) keep invalid_task_identity.

- add taskfailure.ReasonRuntimeAccessDenied (permanent, non-retryable
  ownership authorization failure; agent process never launched)
- swap the reason in the two owner-mismatch settlement paths in
  handler/daemon.go (response-assembly recheck + delivery-gate
  authz-error default branch, including ownerless-runtime denial)
- regression A (queued issue task), B (queued chat task + assistant
  failure message via existing FailTask path), C (agent rebind keeps
  invalid_task_identity)

Co-authored-by: multica-agent <github@multica.ai>

* PUCK-132: map persisted runtime access failures in clients

* fix(i18n): add missing fr translations for runtime_access_denied keys

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: worker-opencode <worker-opencode@multica.local>
Co-authored-by: puck-181 <puck-181@local>
* fix(chat): stream first agent text sooner (MUL-7465)

Co-authored-by: multica-agent <github@multica.ai>

* fix(codex): make delta streaming lossless (MUL-7465)

Co-authored-by: multica-agent <github@multica.ai>

* fix(codex): retain mismatched pending deltas (MUL-7465)

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
…der can't wedge a turn (multica-ai#5878)

Owning the runtime process tree (multica-ai#7522) made cmd.Cancel kill every descendant
in that tree, so the usual pipe holder dies with it and both readers reach EOF.
A descendant outside the tree does not: on POSIX because it called setsid and
left the process group, on Windows because startOwnedProcessTree failed open
and the child runs unowned, so the kill reaches the leader alone. The join
after the forced shutdown was unbounded, so the turn then hung forever: no
tool result, no reply, nothing until the user cancelled by hand.

Reap the process as part of that forced shutdown instead. cmd.Wait() returning
is what closes the parent ends of the pipes, so it frees a reader the kill
could not reach, and the 10s WaitDelay the claude, codearts and antigravity
backends already use bounds Wait itself once the context is cancelled. The
readers are still joined before the buffers are read, so providerErr.Finalize
keeps its documented precondition — a fully drained stderr pipe — instead of
racing the copier and splitting a provider error across its flush. The same
reap now runs in the deferred cleanup, where joining the readers before the
goroutine closes msgCh keeps a live reader from panicking in trySend.

What this buys is a bounded recovery, not a repaired turn: past the existing
2s drain grace the backend delivers from the buffers it has, so an exceptional
shutdown can report a turn without output the readers had not handed over yet.
It does not fix the Windows terminal tool — that belongs in hermes' own bash
startup probe (NousResearch/hermes-agent#73403, still open) — and it does not
reap the escaped process, which is out of the daemon's reach by definition.

Co-authored-by: multica-agent <github@multica.ai>
…ai#8539)

Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
…i#8519)

* fix(issues): preserve quick-create original input

Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): validate quick-create origins

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
…ultica-ai#8538)

* fix(autopilots): make an existing schedule editable again (MUL-7478)

The edit dialog's schedule panel locked whenever an autopilot carried two
triggers of any kind, and pointed at a detail page whose trigger rows were
read-only. Between them there was nowhere in the UI to change a schedule:
the only way out was deleting the autopilot and building it again.

Two halves, and both are needed — the narrowed lock alone would leave the
notice pointing at a page that still cannot edit, and the row editor alone
would leave the most common case (one schedule beside one webhook) taking
the long way round:

- Count only schedule-kind triggers when locking. A single schedule beside
  a webhook has exactly one row a cron can land on, which is the row the
  save path already targets, so there was never an ambiguity to protect.
- Give each schedule row its own editor, opening on the schedule it runs
  and patching that trigger alone. Label and enabled ride along, so pausing
  a schedule no longer means deleting it.
- Replace the greyed-out editor behind the lock with a notice. A disabled
  editor still showed the first of the schedules as if it were the whole
  story; the notice says how many there are and sends the reader to the
  Triggers list already on screen behind the dialog.

Co-authored-by: multica-agent <github@multica.ai>

* fix(autopilots): send only the fields the schedule editor changed

Review found the new per-row dialog writing a full row on every save. Three
consequences, all of them real:

- The server reads a changed cron / timezone / enabled as a substantive edit
  and republishes the rule version, moving the trigger's accountability to
  whoever saved. Since parseCron -> toCron normalizes (a bare cron on a zoned
  row returns carrying its TZ= prefix), even a rename resent a textually
  different expression and took that responsibility with it — the exact
  over-transfer MUL-4302 settled.
- A dialog left open while someone else edited the same trigger wrote its own
  stale reading back over their change.
- A save with nothing edited still made the server recompute and rewrite
  next_run_at from an expression the user never touched.

The dialog now snapshots the trigger at mount, sends only the fields that
moved, and has nothing to save until one does. The cron gate applies only to
a write that carries a cron, so a row whose stored expression the server can
no longer preview can still be switched off.

Two more from the same review:

- The label input stayed editable while the submit validated over the network.
  A label typed in that window was not in the request already in flight and
  vanished under its success toast; it locks with the editor now.
- A disabled trigger keeps its next_run_at — the dispatcher filters on
  `enabled` rather than clearing the column — so the row claimed "Disabled"
  and a next run in the same breath. The badge is the true one.

Co-authored-by: multica-agent <github@multica.ai>

* fix(autopilots): measure schedule edits against the row as it was opened

The dirty checks read `trigger.label` / `trigger.enabled` live off props while
their inputs were seeded once at mount. The detail query refreshes under an
open dialog — a teammate saving this same row — and the component does not
remount, so an untouched control and its prop drifted apart and the field
counted as this user's edit. Save then sent the value they never set, back
over the one that had just landed.

Every baseline is now snapshotted at mount together, and the label baseline is
trimmed the way the submitted value is, so a stored label carrying stray
whitespace does not arm Save the moment the dialog opens.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
…op the skill section (multica-ai#8548)

Co-authored-by: multica-agent <github@multica.ai>
…ltica-ai#8543)

* docs(changelog): add v0.5.0 release entry (2026-09-18) (MUL-7481)

Adds the 0.5.0 changelog entry to all four landing locales and bumps the
web and desktop app versions.

Range v0.4.44 → 594ff89 (55 PRs). Minor bump because the release ships
full French product UI, which is worth announcing on its own.

Left out of the changelog, as agreed on MUL-7481: Triage (triage_v1 not
rolled out), the plugins_v1 / billing_workspace_subscriptions gate error
semantics, UI Lab, CI/test/docs, prompt and skill changes, the Issue
status category migration, and internal performance work.

Co-authored-by: multica-agent <github@multica.ai>

* docs(changelog): retitle the v0.5.0 entry around agent behavior (MUL-7481)

The title listed the top of `features` and carried a small feature
(Autopilot schedule editing) while leaving out the day's agent work.

Drop the small feature, keep the French UI, and put the agent side in:
runs are both steadier (Grok/Pi/Copilot/Codex/Cursor/Hermes, durable
terminal reports) and leaner — a resumed agent no longer re-reads the
whole Issue and every comment (multica-ai#8377, multica-ai#8488). That last one now has its
own improvements line so the title stays backed by the entry.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Bohan-J <bhjiang@outlook.com>
Bohan-J and others added 28 commits September 23, 2026 16:29
…6) (multica-ai#8742)

Wakeup fire times are absolute instants, but the UI formatted them in
the wakeup's stored timezone. The CLI stores UTC by default, so a
one-time wakeup at 14:05Z read as "Today 14:05" for a viewer in UTC+8
instead of 22:05 local, with no timezone label.

Format next_fire_at with the Viewing Timezone preference everywhere
(sidebar summary, details, workspace wakeups list). The stored timezone
still describes cron schedules only.

Closes multica-ai#8740

Co-authored-by: multica-agent <github@multica.ai>
… (MUL-7572, GH multica-ai#8654) (multica-ai#8744)

A Windows user whose network dropped the two-packet TLS ClientHello that
Go 1.24+ sends by default (post-quantum key share, ~1.5 KB) saw only
"Sign-in did not complete: the server could not issue an access token"
and, on the --token path, "make sure it is valid and not expired". Neither
mentioned the network, and the generic timeout copy pointed at
MULTICA_HTTP_TIMEOUT, which does not govern the handshake.

- classify net/http's "TLS handshake timeout" as its own kind, with copy
  that names the GODEBUG=tlsmlkem=0 check in both languages
- let `multica login` keep the transport copy for transport failures; its
  sign-in copy now only overrides HTTP refusals
- troubleshooting entry (en/zh/ja/ko)

Co-authored-by: multica-agent <github@multica.ai>
…riginal (MUL-7349) (multica-ai#8741)

* feat(issues): make duplicate marks traceable and one click from the original (MUL-7349)

- Log duplicate_marked / duplicate_unmarked on the duplicate and
  duplicate_added / duplicate_removed on its original, with the other issue
  linked in the activity feed; the delete and GitHub merge paths now carry
  both ends of the mark so those rows are written too.
- Expose duplicate_of_issue_id on issue responses (only while cancelled) so
  list rows, board cards and table cells can show an arrow to the original
  that opens it directly.
- Banner links as a whole to the original and shows its status; the undo
  action becomes the secondary "Not a duplicate" with a toast.
- Sidebar duplicates show reporters and a count; the status picker reads
  "Change original" on an issue that already is a duplicate; the mark
  picker opens with same-title and recently viewed candidates.

* fix(issues): keep the duplicate pointer in step with CI shape tests (MUL-7349)

- Add duplicate_of_issue_id to the CLI --fields whitelist, which the
  list-endpoint shape test checks against a real response.
- Project the column in the search parity test's legacy query copy and
  scan it, so both queries read through the one scanner.
- Mount the row marker's query and navigation hooks only when the issue
  is a duplicate, so surfaces rendered without a QueryClient (the table's
  inline title) and thousands of ordinary rows subscribe to nothing.

* fix(issues): resolve a duplicate's original on the server and log one row per mark change (MUL-7349)

- Issue responses carry duplicate_of {id, identifier, title, status}, filled
  beside the status category only while the issue is cancelled and the
  original still exists; the bare pointer is no longer exposed. Rows read it
  directly, so the marker makes no request and is a real link where the row
  is not one.
- A mark change suppresses the generic status_changed row; the unmarked row
  records where the status went. Covered through the real UpdateIssue path,
  with the test server wiring activity listeners like main.go.
- The picker applies one selectability predicate to search results and
  suggestions, so an existing duplicate can no longer be chosen.
* fix(agent): refresh live model catalogs

Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): preserve live-only Codex settings on catalog fallback

Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): retain Codex explicit-standard CLI version gate

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
…2) (multica-ai#8748)

Co-authored-by: multica-agent <github@multica.ai>
…ltica-ai#8749)

* docs(changelog): add v0.6.0 release entry (2026-09-23) (MUL-7630)

Co-authored-by: multica-agent <github@multica.ai>

* docs(changelog): switch release entry to v0.5.2 and tighten title (MUL-7630)

Co-authored-by: multica-agent <github@multica.ai>

* docs(changelog): fold steering into a single entry (MUL-7630)

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
…UL-7349) (multica-ai#8752)

* perf(issues): resolve a page's duplicate originals in one read (MUL-7349)

Filling duplicate_of read each distinct original with its own primary-key
lookup, so a page of duplicates of different issues cost one round trip
per row. Pages now pass the originals their rows point at to
newStatusCategoryFiller, which loads them with one ListIssueRefsInWorkspace
read, the same way labelsByIssue loads a page's labels. List, open issues,
grouped, search and table rows do this; a response built on its own still
resolves its original, so a caller that passes nothing is slower, never
wrong.

A pointer left on a reopened issue is not looked up at all, and an
original that no longer exists is looked up once and left off.

Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): stop logging stale duplicate pointers as unmarks (MUL-7349)

Servers from before the duplicate column reopened duplicates and deleted
originals without clearing the pointer. After an upgrade, the next write
to such an issue clears it, and the activity log compared raw pointers,
so it logged "removed duplicate mark" for a mark that no longer counted
and dropped the real status row in its place.

Event publishers now send only live marks: a pointer counts while the
issue is cancelled, the same rule reads apply. Separately, the status row
is dropped only when a duplicate row was actually written to stand in for
it; when the original is gone there is no duplicate row, and the reopen
used to leave no activity at all.

Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): keep duplicate marks in background issue snapshots (MUL-7349)

IssueToMap hardcoded duplicate_of to null on the assumption that no
background publisher renders a marked issue. PublishAttachmentsChanged
does: a channel /issue with media re-reads the issue after a download of
up to 45 seconds, and a mark made in that window was broadcast as null.
Clients patch their cache with that snapshot, so the list and board
marker disappeared until the next refetch.

IssueToMapResolved now resolves duplicate_of the way the HTTP rendering
does; only a cancelled issue with a pointer costs a read.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
…multica-ai#8755)

Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
…-7637) (multica-ai#8757)

* docs(i18n): translate documentation corpus to French (MUL-7637)

Add French (.fr.mdx) translations for all 45 docs pages plus meta.fr.json
navigation, mirroring the English source. Product and UI terms follow the
in-app French locale at packages/views/locales/fr/ (issue -> tâche,
run -> exécution, autopilot -> automatisation, Chat -> Discussion,
Settings -> Paramètres, etc.). Status and role identifiers stay lowercase
English per the conventions glossary, with the French UI label shown once
where they are defined.

Co-authored-by: multica-agent <github@multica.ai>

* feat(docs): serve French docs locale (MUL-7637)

Register fr in the docs i18n config and add the French Fumadocs chrome,
language-switcher label, and home hero copy. Fumadocs maps fr to Orama's
built-in French tokenizer, so search needs no custom localeMap entry.

Co-authored-by: multica-agent <github@multica.ai>

* feat(i18n): link French UI to French docs (MUL-7637)

Route French users from the landing page, runtime guides, the autopilot
webhook filter help, and the Slack/Lark/DingTalk/Telegram setup links to
/docs/fr. The four integration tabs and the runtime helper each carried a
copy of the same locale-prefix ternary; they now share docsLocalePrefix.

Co-authored-by: multica-agent <github@multica.ai>

* fix(i18n): localize remaining docs entry points (MUL-7637)

French users still reached English docs from the Help menu, the Agents and
Skills "Learn more" links, and the landing footer. The views links now use
docsLocalePrefix; the landing dictionaries take the docs href from
docsHrefForLocale, so locales that reuse another dictionary (French reuses
English) still link to their own docs.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
…628) (multica-ai#8746)

* fix(issues): order thread replies by send time, not by trigger (MUL-7628)

multica-ai#8131 took run replies out of the thread's chronological reply list
(multica-ai#4033) and re-inserted each one after the comment that triggered it.
With two agents working in one thread, the agent asked first but
finishing last landed mid-thread, so the newest reply was not at the
bottom.

A run that replied, or ended without replying, now sits at that time
like any comment (MUL-7211). A run still working stays right after the
input it answers, matching how an active standalone run keeps its
enqueue slot (MUL-7632). Everything one run posted still renders
together in posting order (MUL-7548). When a run's input is not the row
directly above, its reply (or activity block) shows a "Replying to
<name>: <preview>" line that scrolls to and flashes that input.

Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): place each run comment in a thread at its own time (MUL-7628)

A run's earlier comments were kept together in its slot at the time of
its latest one (MUL-7548), so a reply written between two of them read
after both. Now that every thread comment sorts by its own time, that
grouping only reorders the conversation: sort each run comment on its
own. The run slot is its latest comment, and the "Replying to" line
looks past the run's own earlier comments when checking what is above.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
…ca-ai#8734)

* feat(antigravity): stream live tool execution events

* chore(i18n): remove obsolete French Antigravity live notice

* fix(antigravity): preserve tool events under backpressure

* fix(antigravity): retain tool errors in bounded previews

* fix(antigravity): normalize tool parameters for transcript UI

* fix(antigravity): move tool aliases and normalize file content
* feat(pr): complete issues when every linked PR merges (MUL-7429)

Title/branch identifiers link a PR; keywords and the body no longer matter.
Completion is evaluated only on a PR event for the issue (merge, link,
unlink), so settings changes and reopening never complete an issue.
Adds manual link/unlink with remembered exclusions and a per-issue
auto-complete switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* feat(pr): issue-page PR automation and single auto-complete setting (MUL-7429)

The issue's Pull requests section says what the merge rule will do next,
links a PR by URL, removes one per row, and turns auto-complete off for
that issue. The workspace switch lives under Issue statuses; the GitHub
page reports it. Agent skill and docs describe the title/branch rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(pr): accept PR links pasted without a scheme (MUL-7429)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* test(skills): pin the PR auto-complete contract in the platform skill (MUL-7429)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(pr): re-check linked PRs at the completion write; skip stale GitHub events (MUL-7429)

The status write now requires every linked PR to still be merged when it
runs, so a PR linked between the decision and the write keeps the issue
open. GitHub PR upserts ignore events older than the stored row, like the
self-hosted path, so a late delivery cannot roll a merged PR back to open
and turn a redelivered merge into a new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
…multica-ai#8763)

* feat(attachments): full-window viewer that pages through every file (MUL-7642)

The preview was a centered card capped at max-w-6xl, so a screenshot never
showed larger than 1152px wide whatever the screen, and prev / next only
walked images.

- The viewer takes the whole window: no card, a near-black stage, a top bar
  in the dark token set, prev / next in 64px side gutters, type / pixel size
  / file size instead of the MIME type. Markdown and text read on a centered
  sheet in the app theme; PDF and HTML fill the stage.
- The surface sequence now covers every previewable kind, so an issue pages
  through its screenshots, PDFs, reports and HTML in render order.
  `collectAttachmentSequence` takes the inclusion rule; mobile keeps the
  images-only `collectImageSequence`.
- The issue description block no longer lists every issue attachment as its
  own: comment uploads keep issue_id, so they were counted at the
  description's position.
- A focused video / field keeps its arrow keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(attachments): viewer image swap and desktop window chrome (MUL-7642)

Review of multica-ai#8763:

- Paging from a PDF (or any non-image) onto an image that had not decoded
  yet handed the PDF's URL to <img>; its load error was blamed on the image
  being opened, which got marked broken and skipped. The settled-URL hook now
  only ever holds a URL that decoded as an image, and the <img> reports errors
  only when it shows the current file.
- On desktop the full-window top bar sat under the macOS traffic lights and
  over the app's top-bar drag region. The viewer now enters immersive mode
  while open, marks itself no-drag, lets its top bar drag the window, and
  opts the bar's controls back out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(attachments): viewer waits for the media re-sign before loading (MUL-7642)

On clients that cannot load `/api/attachments/{id}/download` natively
(desktop, split-origin web, self-host proxy mode) the viewer handed that
endpoint to <img> while the re-sign / authenticated byte fetch was still in
flight. The native load failed, and in a sequence the failure read as a
broken image: it was marked unavailable and skipped before the loadable URL
arrived.

`useResignedInlineMedia` now also reports whether the upgrade is pending.
The viewer holds the previous frame (or shows loading) until it settles, and
the neighbour prefetch waits too. The inline figure keeps its behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(attachments): no focus ring around the viewer stage on open (MUL-7642)

The viewer focuses its zoom canvas on open so +/-/0 and the arrows work
without a click. That programmatic focus can match :focus-visible, and on the
full-window viewer the canvas ring drew a light frame around the whole stage.
The canvas now marks focus it placed itself; the stylesheet drops the ring
for it until focus leaves, so tabbing back in still shows one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
…ultica-ai#8727)

* feat(telegram): send and receive photos, videos and files

Telegram was text-only in both directions: dispatch dropped every non-text
update with an "unsupported" notice, the outbound subscriber only knew
sendMessage/editMessageText, and the channel was never declared to
DeclareChannelFileDelivery, so agents were told they cannot attach files.

Inbound: photos, documents, video, video notes, animations, audio and voice
notes become chat attachments through the engine's MediaResolver seam
(getFile, file host download, object storage, intent-ledger row before the
PUT, as Slack does). The caption is the message text and a placeholder marker
leads the sender's own text; the engine swaps it for the attachment link once
the bind commits. Quoted and recent group context still go in front of it.
Stickers and other non-file kinds keep the unsupported notice.

Outbound: files the agent bound to its reply are sent into the chat as their
own messages once the text settles (sendPhoto for common images up to 10 MB,
sendVideo/sendAudio for mp4 and common audio, sendDocument for the rest up to
50 MB). A photo Telegram refuses to process is resent as a document.

Both halves hang off the same store != nil branch that declares file
delivery, so the agent is promised the hop only where it exists. Compared
with the WeCom reference this drops relay routing, the three-state delivery
classification, per-reply metrics and the pending/admitted counters: Telegram
outbound is stateless HTTP and the delivery lease already guarantees a single
sender per turn.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(telegram): address media review — deliver files once, admit before spawning, keep the notice without storage

Review on multica-ai#8727 raised five points; each lands here with a test.

1. A file-only reply could be delivered twice. closeTurn returned true
   whether this call ended the turn, a deeper attempt owned it, or it was
   settled already, and the empty-reply branch delivered files on all three.
   closeTurn now reports closedNow / closedAlready / closeHeld, and files go
   out only on closedNow. Regression tests: a replayed chat:done on another
   replica sends the files once; a completion an automatic retry has
   superseded sends nothing and the retry's answer still lands.

2. Admission sat behind the goroutine and the lookup. The attachment lookup
   now runs on the terminal worker, so a reply with nothing bound costs one
   indexed read and no goroutine; a delivery is spawned only for files known
   to exist, under a slot claimed first (non-blocking, four slots). Every
   slot busy sheds the reply with the notice instead of parking it.

3. Rebased on main over MUL-7585: recent group context, /new isolation and
   the privacy rule are kept; unaddressed group media is buffered and never a
   turn, as before; the media placeholder leads the sender's own text with
   quoted and recent context still in front.

4. Without object storage the member got no signal. The polling loop now
   knows whether the deployment stores media (ChannelDeps.AcceptsMedia, set
   from the same store != nil branch as the resolver) and keeps the old
   unsupported notice when it does not. A fetch that fails — over the 20 MB
   bot download limit, or a failed download — tells the sender so, instead of
   leaving the agent a placeholder nothing stands behind.

5. The failure notice claimed more than the code knew: a lost response may
   mean Telegram accepted the file. It now says delivery could not be
   confirmed and that the file stays attached in Multica, which holds either
   way.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(telegram): offset the media placeholder past prepended context; report a failed attachment lookup

Second review round on multica-ai#8727.

1. With quoted or recent group context ahead of the sender's text, the
   MediaRef carried only the placeholder, so the engine replaced occurrence
   0: a member who typed "[Image]" in the window received the sender's
   file and the sender's own marker stayed bare. inboundMedia now carries
   PlaceholderIndex — the enrichers only prepend, so it is the count of the
   marker ahead of the sender's segment, literals included (the DingTalk
   rule) — and the resolver sets it as InlineIndex. Regression tests cover
   the recent-context and the quoted-message paths.

2. A failed ListAttachmentsByChatMessage dropped the files silently while
   the text already on screen referred to them. The read has no side
   effects, so it is retried three times 250 ms apart; if it still fails
   the member gets a notice worded for what is known — whether the reply
   had files could not be checked — never the one that presumes a file
   existed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…a Helm (MUL-7639) (multica-ai#8761)

* fix(vcs): report untrusted provider TLS certificates on connect (MUL-7639)

ConnectVCS reported every non-token failure as "could not reach the
provider instance" and logged nothing, so a Gitea behind a private CA
looked like a network problem. Log the underlying validation error and
tell certificate failures (untrusted CA, host name mismatch, other
verification failures such as expiry) apart from unreachable instances.
Status codes are unchanged.

Co-authored-by: multica-agent <github@multica.ai>

* feat(helm): trust extra CA certificates in the backend (MUL-7639)

backend.extraCACerts.configMap mounts an existing ConfigMap of PEM
certificates read-only and points SSL_CERT_DIR at the system directory
plus the mount, so the backend trusts an internal CA without dropping
public CAs or disabling TLS verification. Unset renders unchanged.

Co-authored-by: multica-agent <github@multica.ai>

* docs(self-host): document trusting a private CA (MUL-7639)

Co-authored-by: multica-agent <github@multica.ai>

* fix(helm): render when values predate extraCACerts (MUL-7639)

helm upgrade --reuse-values from an older chart carries no
backend.extraCACerts key, and reading .configMap on it failed the whole
render with a nil pointer even when no CA was wanted. Fall back to an
empty dict, and cover the missing-key, default and enabled renders in
the chart test.

Co-authored-by: multica-agent <github@multica.ai>

* docs(self-host): restart Compose backend after replacing a CA (MUL-7639)

up -d keeps the running container when only a mounted CA file changed,
so the backend kept its old trust store. Use restart instead.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
…i#8702)

Add a standalone Simplified Chinese UI to the mobile app, with a
Settings → Language picker (Follow system / English / 简体中文) whose
manual choice persists across restarts. Mobile keeps its own i18next
resources rather than reusing web/desktop copy, following the
conventions.mdx glossary: issue statuses and categories stay lowercase
English identifiers, Squad is 小队.

- Failure reasons: REASONS now mirrors web's REASON_LABEL, and a drift
  test pins it to the en/zh-Hans failure_reason keys.
- Production env: .env.production stays committed with the public
  multica.ai defaults; the *:prod scripts layer a gitignored
  .env.production.local on top for self-hosting or a custom bundle ID.
- Duplicate-mark activity rows from multica-ai#8741 are localized too.
…ca-ai#8657)

* feat(llm): support MULTICA_LLM_DISABLE_THINKING env

Co-authored-by: multica-agent <github@multica.ai>

* docs(llm): scope reasoning_effort note to quick actions

The disable-thinking docs claimed GPT-5.6-family models already get reasoning_effort=none from the server and do not need the new switch. That is only true for quick actions (GenerateJSON); chat auto-titling goes through GenerateText, which sets no reasoning field. Narrow the wording in .env.example and all four language docs so the translations stay in sync.

Co-authored-by: multica-agent <github@multica.ai>

* docs(llm): scope the disable-thinking note to accepting upstreams

The previous wording called the switch "the only way" to turn off auto-titling
reasoning, but on a standard OpenAI endpoint the switch adds chat_template_kwargs
to the title request and the upstream rejects it, so titles fail instead of
skipping thinking. Drop the "only way" claim and name the feature the way the
list below does (follow-up questions).

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: zhudejun1 <zhudejun1@huya.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Multica Agent <agent@multica.local>
…multica-ai#8790)

The French pages from multica-ai#8757 were translated before multica-ai#8727 landed, so both
still said the Telegram bot only accepts text. Translate the current English
passages, reusing the terms the French pages already use.
…tica-ai#8792)

* fix(claude): report per-run usage for resumed sessions

Co-authored-by: multica-agent <github@multica.ai>

* fix(claude): preserve usage when counters reset

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
…available wording (MUL-7558) (multica-ai#8643)

* docs(license): name Index Labs (Hong Kong) Limited as the producer (MUL-7558)

The LICENSE and NOTICE credited "Multica, Inc.", which is not a registered
entity. Replace it with the company that holds the rights, define "the
producer" (used throughout Part I but never defined), and point to where
commercial licenses and branding waivers are requested.

Co-authored-by: multica-agent <github@multica.ai>

* feat(web): add licensing and privacy pages, describe Multica as source-available (MUL-7558)

- /licensing: plain-language licensing FAQ with the rule of thumb and common
  scenarios, in en/zh/ja/ko.
- /privacy: privacy policy based on what Multica Cloud actually collects;
  the Contact Sales privacy link now points here instead of /about.
- About: new "Who's behind Multica" section (team story + contact routes;
  team member cards left for later).
- Replace "open source" / "fully open source" with source-available
  wording across the landing copy, page metadata, JSON-LD, docs, READMEs,
  and the Helper agent prompt, since the license restricts hosted use.
- Contact Sales consent copy says "Multica" instead of "Multica, Inc.".
- Reserve the "licensing" workspace slug for the new top-level route.

Co-authored-by: multica-agent <github@multica.ai>

* fix(web): drop the Slack users section from the licensing page (MUL-7558)

Co-authored-by: multica-agent <github@multica.ai>

* fix(web): add the commercial-use FAQ in ja/ko and sharpen the source block headline (MUL-7558)

- ja and ko override faq.items wholesale, so the new "Can I use Multica
  commercially?" entry (the FAQ's only link to /licensing) was missing
  there. Add it, plus a test that keeps every locale's FAQ in step.
- The source block headline now states the value ("Every line, on your
  terms.") instead of repeating the license category.
- Privacy policy: list apps connected through Composio among the
  integrations that receive data.

Co-authored-by: multica-agent <github@multica.ai>

* fix(web): align the privacy policy with what the product actually does (MUL-7558)

Privacy review found promises the code does not keep:

- Workspace deletion removes content from the service, but uploaded files
  are not yet erased from object storage and backups keep copies. Say so,
  and give an email route for erasing files.
- AI: coding agents send prompts, code, and tool results to their own model
  providers; only the Cloud assist features use a provider we pick. Scope
  the training promise to Multica.
- Crash reports: redaction filters recognizable emails and credentials in
  the error message only, not every field.
- Self-hosted: list the snapshot fields (including the random deployment
  ID), and note that AI, integrations, and analytics follow the operator's
  configuration.
- Sharing: cover workspace members, admins, and authorized agents; move
  legal and M&A disclosures out of the service provider list.
- Add legal bases, consent withdrawal, and the right to complain, plus
  retention for billing records and analytics.

Co-authored-by: multica-agent <github@multica.ai>

* fix(web): build trust-page test dictionaries through createLandingDict (MUL-7558)

main now passes a docs href to each landing dictionary factory, so the
test's single-argument calls failed typecheck. Build the dictionaries the
way the app does, mark the DOM-free test as node, and bring the new French
mobile-app doc in line with the source-available wording.

Co-authored-by: multica-agent <github@multica.ai>

* fix(web): stop overpromising on data handling and align trust copy with product terms (MUL-7558)

- The commercial-use FAQ now names both license triggers in every locale
  (offering Multica to outside users, and embedding it in a product you sell
  or distribute); ja and ko read as hosted-only before.
- Replace "your data never leaves your network" and "code never passes
  through Multica servers" with what actually happens: agents run on your
  machines, workspace content is stored by Multica, coding tools send
  prompts to their model providers, and self-hosting keeps workspace data on
  your servers. Links to the privacy policy.
- New and changed copy follows the terminology conventions: issue ->
  任务/タスク/태스크, agent run -> 运行/実行/실행 (Run in English),
  workspace -> 工作区, onboarding -> 上手引导.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
…ica-ai#8794)

* feat(pr): only a closing keyword completes an issue on merge (MUL-7672)

multica-ai#8758 made every linked PR count toward auto-complete, so a title-only
"MUL-123: ..." PR moved its issue to Done on merge. Restore the earlier
contract: a PR completes its issue only when its title or body puts a
closing keyword (Closes/Fixes/Resolves) right before the identifier.

- Body closing keywords link again; a bare body mention still links nothing.
- close_intent is recorded on the link and follows the PR text until the
  merge/close event, then freezes; a link first made after merge carries none.
- The decision requires every linked PR merged and at least one with close
  intent; a new no_close_intent state explains why a merge won't complete.
- Everything else from multica-ai#8758 stays: manual link/remove, per-issue and
  workspace switches, edge-triggered evaluation, the result line.
- Docs (4 languages), UI copy (5 languages) and the multica-platform skill
  describe the keyword rule again.

Co-authored-by: multica-agent <github@multica.ai>

* fix(pr): sync close intent on every link of the PR (MUL-7672)

Review of multica-ai#8794 found two ways a withdrawn closing keyword still completed
the issue:

- close_intent was refreshed only for identifiers the PR still claimed, so
  a manual link, or an automatic link whose merge event (carrying the final
  text) arrived before the edit event, kept a stale true.
- the refresh sat behind the auto-link toggle, so turning auto-link off
  froze whatever intent was recorded.

Replace the per-link update with one PR-wide sync: until the merge/close
event, every link of the PR (automatic or manual) gets close_intent exactly
when the current title/body closes its issue. It runs whenever GitHub
features are on, independent of auto-link, which now only decides which
links are created.

Co-authored-by: multica-agent <github@multica.ai>

* fix(pr): decide close intent apart from auto-link ownership (MUL-7672)

Re-review of multica-ai#8794: the PR-wide close intent sync reused the auto-link
verdict, and resolvePRLinkPolicy skips workspaces with auto-link off. With
an installation bound to several workspaces, turning auto-link off in one
of them therefore cleared a closing keyword only that workspace resolves,
and the merge no longer completed the issue.

The policy now reads every bound workspace with GitHub on, and records
the identifier's sole resolver next to the auto-linking owner. Links still
follow the owner (auto-link off workspaces are not competing claimants);
close intent is permitted for the owner or the sole resolver, so an
identifier that resolves in more than one workspace stays withheld.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
…ltica-ai#8688)

* fix(daemon): expose original requester in agent context

* fix(daemon): render run originator as on-behalf-of identity

* docs(daemon): update stale Task Initiator comments to On Behalf Of

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bhjiang@outlook.com>
Co-authored-by: multica-agent <github@multica.ai>

@amazon-q-developer amazon-q-developer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This upstream merge brings in 122 commits and 77 database migrations (schema 467→547). The code changes I reviewed function correctly. Key observations:

Production deployment carries significant risk due to the migration scope. The PR description documents the necessary precautions: maintain database backups, keep workers stopped during migration, and have a database restore plan ready since image-only rollback is insufficient.

Verify that the Singapore deployment environment variables (CODERPUSH_BACKEND_IMAGE and CODERPUSH_WEB_IMAGE) are set to verified digest references before deploying.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.


⚠️ This PR contains more than 30 files. Amazon Q is better at reviewing smaller PRs, and may miss issues in larger changesets.

@harley
harley merged commit 355006f into main Sep 24, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.