Commit Graph
2198 Commits
Author SHA1 Message Date
a319c2f482 feat(daemon): capture what each run changed and upload it before reporting done (MUL-7651)
- Local worktree mode: Finalize now reports the range it delivered
  (baseline..tip), so the run's change is exactly the agent's commits, with
  the user's replayed edits on the baseline side. The branch row runs from
  the newest baseline on the branch, the start of this line of work.
- Repository checkouts: each checkout's HEAD is recorded when the run first
  sees it (already in a reused workdir, or made by `multica repo checkout`),
  and diffed against HEAD at the end. The branch row measures against the
  remote's default branch.
- codechange diffs two commits with user diff config pinned off (external
  drivers, textconv, prefixes, colour), caps the patch at 1 MB and the file
  list at 3000, and stops git rather than draining a larger patch.
- Collected on every exit path, failed and cancelled runs included, and
  uploaded before the terminal report. Best effort: an older server's 404
  is logged and ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-27 04:26:58 +08:00
3c75f03a76 feat(server): store each run's code change and serve it to the issue page (MUL-7651)
A run's diff arrives from the daemon as it ends and lands in task_code_change:
one row per run, scope and repository. "run" is the run's own change;
"branch" is the delivered branch against where its line of work started,
which backs the issue-wide view when no pull request can supply it. The
patch goes to object storage; a patch over 1 MB never arrives and the row
keeps only the file list.

- POST /api/daemon/tasks/{taskId}/code-changes stores them. First write
  wins, so a retried upload changes nothing. Remote URLs lose any embedded
  credentials; commits, statuses and sizes are validated and clamped.
- GET /api/issues/{id}/code-changes lists summaries, and
  /code-changes/{changeId} returns one with its file list and patch.
- GET /api/issues/{id}/pull-requests/{prId}/diff reads a linked GitHub PR's
  files through the GitHub App and assembles them into one patch.
- code_changes:created tells open issue pages to refetch.
- No foreign keys: issue delete and workspace delete remove the rows and
  their stored patches.
- .patch and .diff join the text preview whitelist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-27 04:26:58 +08:00
6dc6a0b9a2 MUL-7650: attachment viewer — CSV tables, JSON/YAML tree, numbered code, HTML viewports, every kind in a new tab (#8868)
* feat(server): accept JSON Lines in the attachment text preview proxy

The viewer now shows .jsonl / .ndjson files as a tree, so the text proxy's
whitelist takes them too (extension and application/x-ndjson). The whitelist
test gains the CSV / TSV / log / JSON Lines cases and says it is mirrored by
the client-side table in packages/views/editor/utils/preview.test.ts.

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

* feat(views): table, tree, numbered-line and viewport views in the attachment viewer

MUL-7650, step 3 of MUL-7642. Fills in how the full-window viewer shows the
file types agents produce most.

- CSV / TSV (`table` kind): parsed to a table on the shared DataTable —
  pinned header, virtualized rows, resizable columns, a frozen row-number
  column. Clicking a header sorts ascending / descending / file order;
  numbers sort numerically, empty cells stay last, all-number columns align
  right. `;`-separated exports are sniffed. DataTable headers now carry
  aria-sort when the table sorts through TanStack's own state.
- JSON / JSON Lines / YAML (`structured` kind): a collapsible tree with
  inline previews of collapsed records and paged long lists; Tree / Raw in
  the top bar. A file that does not parse falls back to the source and says
  why. YAML alias expansion stays capped by the parser.
- Code, text and logs: numbered lines (the number is a pseudo-element, so
  copying skips it) with a sticky gutter, and a wrap toggle. Logs and plain
  text wrap by default, code does not; the reader's choice carries across
  the sequence.
- HTML: Fit / Desktop 1440 / Tablet 768 / Phone 390. A device width lays the
  document out at that width and scales it down when the stage is narrower,
  with the size and scale shown under it; switching never remounts the
  iframe. A source toggle shows the HTML with line numbers.
- Open in new tab works for every previewable kind: /{slug}/attachments/{id}
  /preview loads the record by id and renders the viewer's own top bar and
  stage (AttachmentPreviewStandalone), keeping the desktop scroll restore for
  HTML.
- Whitelist: .jsonl / .ndjson join the text types on both sides, and the
  client test mirrors the server's case table.

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>
2026-09-27 04:13:06 +08:00
7dac88a6a8 MUL-7649: issue deliverables and dynamic blocks — sidebar section, overview grid, viewer info panel, html/mermaid block frame (#8779)
* feat(attachments): issue deliverables model and sequence block ids (MUL-7649)

collectDeliverableFiles groups every comment upload into deliverables,
merging same-name, same-type re-uploads into versions (newest version
first). Description attachments are inputs and never enter the model.

Sequence items now carry the id of the block they came from, so a viewer
can say which comment (or the description) a file was posted in.

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

* feat(viewer): host details, overview and info panel slots (MUL-7649)

PreviewSequenceProvider takes an optional describeItem (title accessory,
info panel, locate action) and onOpenOverview. The viewer renders only the
slots it is handed: a locate button, a grid button (G) and an info toggle
(I) whose panel sits beside the stage. Letter keys stand down in fields,
and any key pressed inside an open menu stays with the menu.

A zoom canvas still at fit now follows a viewport resize (the info panel
opening), while a zoom the reader chose is only clamped, as before.

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

* feat(issues): deliverables section, overview grid and info panel (MUL-7649)

The sidebar's "Deliverables" section shows what the issue delivered as a
whole: its pull requests and the files its comments uploaded, re-uploads
merged into one entry marked v2. It lists the three newest images and four
newest files, plus "View all N deliverables".

The former Pull requests section becomes the code group, keeping its
MUL-7429 actions (link a PR by URL, the auto-complete menu) beside the
group label. While the workspace shows PRs, the section stays up even with
nothing delivered, since that is where a PR gets linked by hand.

The overview (from "view all" or G in the viewer) shows code first, then
files grouped by posting comment, filterable by kind, each group one click
from its comment. Its count always equals the sidebar's.

In the viewer, deliverables get a version switcher, and every file gets an
info panel (source excerpt, sibling files, uploader, time, size) and
"Show in comments", which unfolds a resolved thread if needed and reuses
the timeline's jump-and-flash highlight.

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

* feat(comments): standalone attachments as cards and an image row (MUL-7649)

The files under a comment (and a chat message) no longer stack as one
full-width row each. Other files become cards in a grid: type glyph, name,
"TYPE · size", download / remove on hover, and the whole card opens the
file. Several images sit in one row at a shared height; a lone image keeps
its full size. HTML keeps its embedded preview (MUL-2330).

On the issue page a re-uploaded file's card carries its version (v1, v2)
from the deliverables model, through a small context.

Standalone attachments are grouped images, then HTML, then files, and the
preview sequence walks them in that same order, so paging follows the
screen. The file-type glyph moves to editor/utils/file-icon so comment
cards and the deliverables surfaces share one mapping.

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

* refactor(issues): pull requests keep their own section (MUL-7649)

Since MUL-7429 the PR block is issue workflow — linking a PR by hand, the
auto-complete switch, "won't auto-complete: missing Closes" — rather than
output, so it goes back to main's own Pull requests section, unchanged,
directly above Deliverables.

Deliverables is now the delivered files only: hidden until a comment
delivers one, and the sidebar number, "View all N" and the overview's
"All N" all count files. The overview drops its code group.

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

* fix(comments): lay several images out as justified rows (MUL-7649)

Fixed-height tiles wrapped as soon as the column narrowed — three
screenshots became two plus an orphan, with a ragged right edge. Rows are
now justified from each image's real aspect ratio: every row shares one
height and a full row runs edge to edge. A row takes as many images as fit
above a minimum height, never grows past a maximum, and a last row that
does not fill keeps the height of the row above it.

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

* feat(rich-content): dynamic block frame for html and mermaid fences (MUL-7649)

A fenced ```html or ```mermaid block is part of the message body, so it now
renders in one shared frame instead of two unrelated widgets:

- Always-visible title bar: kind icon, the fence's title="..." (or the kind
  name), a kind chip, Preview | Source, fullscreen and copy. Previously the
  controls only appeared on hover and floated over the content.
- The body takes its content's height (at least 120px) and collapses past
  480px behind a fade and "Show all". An HTML block used to be a fixed 480px.
- Loading keeps the height this block had earlier in the session; a script
  error or a Mermaid parse error is explained inside the frame with
  "View source" and "Copy error".
- HTML gets the app's theme tokens (--foreground, --chart-1 ...); HTML that
  uses one also gets the app's color-scheme, so it follows dark mode. HTML
  that uses none keeps its own look.

The sandboxed document reports its height and first uncaught error through a
one-line postMessage bridge; the page checks the message source and clamps
every value, and stops a document whose height follows the frame. rehype-raw
drops the fence meta, so the title is read in the closed-fence parse.

The Mermaid sandbox now also declares the app's color-scheme and font: in
dark mode it was painted on an opaque white canvas, and its labels were drawn
in a serif font that did not match the layout Mermaid measured.

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

* feat(attachments): HTML files show as cards, not inline previews (MUL-7649)

Reverses the MUL-2330 pin. An uploaded HTML file is a deliverable to open,
not part of the text: like every other non-image file it shows as a card
(a row inline in the body, a grid card under it) that opens in the viewer,
and its contents are no longer fetched to embed a 480px iframe. HTML meant
to be read in place is written as a ```html block, which renders as a
dynamic block.

- Attachment drops its html branch; HtmlAttachmentPreview is deleted and
  HtmlPreviewBody keeps only the inline source the viewer uses.
- orderStandaloneAttachments groups images, then files (HTML included), so
  AttachmentList and the viewer sequence still walk the same order.
- The MUL-2330 regression pins now assert the card and that no text fetch
  happens.

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

* docs: charts go in html/mermaid blocks, files show as cards (MUL-7649)

Agents that uploaded an HTML chart expecting it to render in the comment now
get a file card instead. Tell them where each kind of content goes:

- multica-platform skill (issues reference, routed from the table): a fenced
  ```html / ```mermaid block renders in place with a title bar and content
  height; an attached file, HTML included, is a card that opens the viewer.
  Covers title="...", the sandbox, the theme variables and their opt-in
  color scheme, and sizing to content rather than the viewport.
- `multica issue comment add --attachment` help says the same in one line.
- Comments docs (all five languages) describe both.

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

* feat(daemon): runtime brief says where charts and diagrams go (MUL-7649)

The chart/file rule lived only in the multica-platform skill's issues
reference, which an agent opens on demand, and the skill's description did
not mention comment formatting, so an agent that just wanted a chart in its
result had no reason to read it. Agents that uploaded report.html expecting
an inline chart now get a card.

- The brief's Output section carries one line, beside the file-delivery
  line, on the surfaces the web renders (issue comments and web/mobile
  chat): put charts and diagrams in the text as a fenced html or mermaid
  block, name it with title="...", and an attached file, HTML included,
  shows as a card. Theming and sizing stay in the reference.
- Channel chats, autopilot run results and quick-create stdout do not render
  these blocks and do not get the line; the delivery tests pin both sides.
- The skill description names "charts and files in comments" so the
  reference is picked for that task.

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

* fix(issues): locate a file in a collapsed thread (MUL-7649)

"Show in comments" for a file posted in a reply only unfolded resolved
threads and gave up after 30 frames, so a thread the reader had collapsed
stayed shut and the reply never mounted. A reply now goes through the
quick-jump rail's jumpToReply, which already undoes every kind of folding
and waits for the reply to land. A root comment gets the same treatment for
its own thread (the reader's collapse, or a resolved bar), then the jump.

Found in review by Emacs.

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

* fix(viewer): the info panel's "Show in comments" closes the viewer (MUL-7649)

The top bar's locate went through the viewer, which closes itself first;
the same button in the info panel called the host's callback directly, so
the page scrolled underneath a viewer that still covered it. Controls handed
to describeItem gain close(), and the issue page builds one close-then-locate
action for both entry points.

Found in review by Emacs.

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

* fix(issues): the deliverables overview is a real modal dialog (MUL-7649)

The overview was a portaled layer with role="dialog" and nothing else: focus
stayed on the opener, Tab walked into the page underneath, and closing left
focus wherever it was. It now renders through the shared Dialog, restyled to
cover the window, so the primitive moves focus in, keeps it inside, returns
it to the opener, and handles Escape. `G` back to the viewer's file stays.
The dialog is named by its title ("MUL-123 deliverables").

Found in review by Emacs.

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>
2026-09-27 03:23:21 +08:00
fc4a615700 MUL-7726: let the workspace choose the status merged PRs move an issue to (#8862)
* feat(pr): move issues to a workspace-chosen status when their PRs merge (MUL-7726)

The merge automation completed an issue only when a PR said "Closes MUL-1",
so the workspace switch mostly did nothing: most PRs are linked by title or
branch and never completed anything. The workspace now picks what a merge
does, in settings.pr_merge_status: "none", or the key of a started or done
status (custom ones included, Blocked excluded). Absent means Done. When
every linked PR is merged, the issue moves to that status. A closing keyword
only links.

- Decision: terminal, triage, "none", the per-issue switch and a new
  at_target state (the issue already has the target) come before the PR
  states, so the issue page only speaks when a merge would move the issue.
  The response adds target_status.
- MoveIssueFromPullRequests replaces CompleteIssueFromPullRequests and
  writes the target status. The target is resolved through the delivery's
  shared catalog read (Resolver.WritableCategory). An archived or unknown
  choice, or a failed read, fails closed to no write.
- Close intent is no longer synced or read. The column stays.
- Migration 551 pins "none" on existing workspaces whose history shows a
  merge never moved every issue: auto-complete switched off, or a linked PR
  whose merges were not all keyword merges. Workspaces with no links, or with
  only keyword merges, keep the Done default. Replays are harmless.
- CLI: `multica issue pr-automation <id> on|off` lets an agent keep one issue
  where it is. The multica-platform skill describes the new rule.

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

* feat(views): choose the status merged PRs move an issue to on the GitHub page (MUL-7726)

- Settings → Integrations → GitHub → Features: "After PRs merge, move the
  issue to" picks Don't change, or a started or done status, custom ones
  included and Blocked excluded. It replaces the read-only auto-complete row.
  The self-hosted Git page shows the same setting. The statuses page drops
  its switch, and a badge on the target status now links to the setting.
- Issue sidebar: the line under the PR list names the target status ("Moves
  to In Review when #19 merges"). It says nothing when the workspace leaves
  status alone, when the issue already has the target, or on an older
  backend's no_close_intent. The per-issue switch reads "Keep status when PRs
  merge" and is hidden when it would change nothing. The menu opens the
  GitHub setting. The link hint only shows while auto-link is on.
- core: derivePRMergeStatus replaces derivePRAutoCompleteEnabled; the
  auto_complete schema adds target_status and the at_target state.
- Copy in 5 languages.

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

* docs: describe the workspace-chosen status for merged PRs (MUL-7726)

GitHub integration, self-hosted Git, issues and environment variable pages
(4-5 languages) now say that the workspace picks what a merge does: Done by
default, another started or done status, or no change. They also say that a
closing keyword only links, and how to keep one issue's status. The
troubleshooting entries explain what a missing line under the PR list means.

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

* refactor(pr): keep the merge automation out of agents' instructions (MUL-7726)

Agents don't act on what a merge does to an issue, so they don't need a
command for it or a rule to learn. Drop `multica issue pr-automation`, and
cut the multica-platform skill down to how PRs get linked: no merge rule, no
auto_complete field guide, no "the server moves it to done" note. People
still keep one issue's status from its Pull requests menu, and the docs now
point there.

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

* fix(pr): keep the retired auto-complete switch working for old desktop clients (MUL-7726)

Review of #8862: an admin on a desktop client from before this change can
still flip "Complete issues when their PRs merge". The client saved
pr_auto_complete_enabled = false, and the save looked successful, but the
server only read pr_merge_status, so merges kept moving issues to Done.

- Settings writes: a flip of the retired switch becomes a choice (off →
  none, on → Done). An old client that echoes a chosen target without
  flipping the switch leaves the target alone. Every write mirrors the switch
  from the effective choice (false for none, absent otherwise), so old
  clients show the right state.
- Reads: without pr_merge_status, the retired switch decides (off → none,
  else Done), on the server and in derivePRMergeStatus. This also covers
  settings written by a pod that predates the change during rollout.
- Migration 551 sets the switch off on the workspaces it pins to none.

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>
2026-09-27 02:26:24 +08:00
Xichang(Seacen) Zhao 7551ff1ddc fix(wecom): answer an unreadable message once per message, not once per delivery (#8836)
WeCom redelivers a callback it did not get an ack for, and this receipt is sent
from dispatchFrame — before c.handler, so the Router's own Claim never runs for
it and nothing downstream deduplicates it. Unclaimed, every redelivery of one
unreadable photo puts another apology in the chat, and each copy spends one of
that conversation's active pushes.

The claim is the same two-phase token the Router uses, on the same table and the
same (installation, message) key, so a message answered here can never also be
answered there. NewDeduper is exported for exactly that reason: router.go and
NewResolverSet now build it the same way instead of each reaching for the
private type.

Three outcomes, chosen so the user's worst case is today's behaviour: already
claimed says nothing; sent marks it; a FAILED send releases, because holding the
claim over a receipt that never arrived turns one failed send into permanent
silence.

The wiring is the part that has to hold. RegisterWecom warns when Dedup is nil,
since a missing one has no other symptom, and two tests cover the two ways it
can go missing: TestRegisterWecomCarriesTheDeduperIntoTheChannel drives
newWecomFactory, and TestWecomChannelGetsItsDeduperOnTheRealBootPath reads
NewRouter itself with the anti-vacuity check taken as a delta between two
routers.
2026-09-27 02:05:04 +08:00
Bohan Jiangandmultica-agent 12f8f3f311 feat(agent): list Claude Opus 5.5 and GPT-6 Sol/Luna in the fallback catalogs (#8840)
Since the fallback catalogs only feed the model picker (#8811), listing
current models there is cheap. Add the models the installed CLIs now
advertise but the static lists were missing:

- Claude: claude-opus-5-5 (Claude Code 2.1.280 list_models default)
- Codex: gpt-6-sol (low..ultra) and gpt-6-luna (low..max), both default
  medium, from codex-cli 0.155.1 `debug models`; live discovery labels
  them "GPT-6 Sol" / "GPT-6 Luna" like GPT-6 Astra

MUL-7691

Co-authored-by: multica-agent <github@multica.ai>
2026-09-25 14:43:00 +08:00
Xichang(Seacen) ZhaoandBohan-J 005462ec22 MUL-7705 docs(channels): correct comments main's own later changes made false (#8833)
Three comments described behaviour that later changes on main replaced.
Comments only; no behaviour change.

- slack/typing_indicator.go: EventTaskFailed carries the chat session id on
  the envelope like EventTaskCancelled (both go through service.taskEvent),
  not only in the payload map. chat:done's payload is a ChatDonePayload
  struct, so the helper's map read only ever matches the two task events.
- wecom/types.go: outbound no longer needs a single replica when a
  sharded/dual realtime relay routes off-lease replies to the lease holder
  (#7429); legacy relay mode or no Redis still does. Also documents the
  per-chat quota gate (#8345): a slight overage is delayed, a burst past the
  caller's wait budget is refused before the write.
- cmd/server/router.go: the boot-time SINGLE-REPLICA CONSTRAINT note now
  states the relay-dependent topology.

Co-authored-by: Bohan-J <bhjiang@outlook.com>
2026-09-25 14:36:02 +08:00
Bohan Jiangandmultica-agent 1aa815a738 fix(daemon): stop validating saved model settings against fallback catalogs (#8811)
When model discovery fails, providers substitute a static catalog so the
picker stays usable. The daemon then validated saved thinking levels and
service tiers against that stand-in, silently dropping values the static
list did not know about — which forced hand-maintained model and per-model
effort tables to chase every new release.

Only a verified catalog (discovered, non-empty) may now reject, rewrite, or
drop a saved model, thinking level, or service tier. Fallback and empty
catalogs pass the saved values through to the CLI for every provider. The
checks that describe the binary rather than its models stay: a Claude CLI
without --effort (or without the saved value in its --effort list) still
drops the level, and Codex's explicit standard tier still follows the CLI
version.

- Catalog.Verified / Catalog.CLIThinkingLevels; missingFromFallbackCatalog removed
- claudeModelEffortAllow removed: the fallback picker offers the --help superset
- ModelKnownIncompatibleWithProvider no longer consults static lists (prefix-only, same result)

MUL-7691

Co-authored-by: multica-agent <github@multica.ai>
2026-09-25 14:33:58 +08:00
dbfc014bdd MUL-7610: fix(autopilot): avoid loading task history during webhook recovery (#8710)
* fix(autopilot): avoid loading task history during webhook recovery

* refactor(autopilot): move webhook recovery task check into agent.sql

Rename HasTasksByIssue to HasTaskForIssue and place it beside the other
per-issue task existence queries. Group test imports.

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

---------

Co-authored-by: Bohan-J <bhjiang@outlook.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-25 14:30:50 +08:00
pseudoyu 64b06b3de8 fix(telegram): attach the file a reply quotes (#8828)
A reply that quoted a photo, video or file reached the agent without the
file: in a group with an @-mention the quote rendered as
"[empty or non-text message]", and in a private chat, or when replying to
the bot's own message, the quoted message was not rendered at all. Only
the trigger message's own file went through the media resolver.

Telegram delivers the quoted message in reply_to_message with its file
ids, so the adapter now picks that file the way it picks the sender's own,
renders it in the quoted block as its placeholder above the caption, and
carries both files in the raw envelope in body order. The resolver ingests
every file, keys each object by its position as DingTalk and WeCom do, and
tells the sender once when one could not be fetched. A quoted file is
selected context in every chat and whoever sent it, as long as the reply
addresses the bot; quoted text keeps the group-mention rule.

Recent-context entries render a file the same way, so a captioned photo in
the window no longer hides that a photo was there.
2026-09-25 14:29:30 +08:00
e2f4a22030 feat: steer a running agent from the normal composer, per recipient (MUL-7631) (#8760)
* feat(server): steer running turns from ordinary comments (MUL-7631)

A comment can now name recipients whose running turn should receive it
(`steer_agent_ids` on POST /issues/:id/comments). Each named recipient
with a running, steer-capable turn gets the comment bound to that turn
instead of a follow-up run; any other recipient keeps its normal
queued / coalesced / deferred trigger, so losing a race never drops input.

- One comment can steer several turns: receipts are keyed by
  (comment_id, task_id) and returned as `supplements` on comments and
  timeline entries. The single supplement_* fields mirror the first
  receipt for older clients.
- Completion reconciliation skips a steered comment only for the agent
  it steered, so the same comment still reaches recipients that were
  addressed without steering.
- Delivery claims match the (comment, run) pair.

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

* feat(issues): choose per recipient how a message reaches a running agent (MUL-7631)

Steering moves from a separate "Add message" box on the run row into the
normal reply and comment composers. Recipients keep the existing trigger
rules; each recipient's current run on the issue decides what the message
can do:

- running (and steer-capable): add to the current run (default in that
  run's own thread), start after this run, stop and start over, or skip
- queued: include when it starts, or skip
- idle: start on send, or skip

Receipts follow the message ("Waiting for Lambda to read it" → "Read by
Lambda · after step N"), a failed delivery offers retry or sending it as
a new run, and the run's step list marks where it read the message.
A draft whose recipient finished meanwhile stays in place and says it
will start a new run; a message with files is handled after the run.

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

* fix(server): steer only the chosen turn, and make a retried steer idempotent (MUL-7631)

- Steering names the exact running turns the author chose
  (`steer_task_ids`). A chosen turn that ended before the send arrives is
  never swapped for a later turn of the same agent; that recipient keeps
  its normal trigger.
- A steering send carries `client_request_id`. A retry after a lost
  response returns the original comment (200) instead of creating a second
  comment and a second delivery; a concurrent twin binding the same request
  is treated as already steered.

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

* fix(issues): guard steering sends against stale previews and broad resends (MUL-7631)

- "Stop and start over" never stops a run while the trigger preview is
  still catching up with an edited @mention: the stop is dropped unless the
  preview answers the submitted text.
- Steering sends the chosen turns' task ids and one request id per draft
  text, reused on retries of the same text.
- "Send as a new run" on a failed receipt reaches only that receipt's
  agent: every other current recipient is suppressed.

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

* fix(issues): keep the reply box placeholder neutral (MUL-7631)

The empty reply box cannot know who a reply goes to: the author may
@ someone else or only talk to a teammate. The recipient chip already
says what Send will do once there is text.

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

* test(issues): give steering composer tests room for the preview debounce (MUL-7631)

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

* feat(issues): choose the default reply to a running agent (MUL-7631)

Preferences > Comments gains "When replying to a running agent":
"Add to current run" (default) or "Start after this run". It only
changes what a reply does without a per-message choice; the recipient
chip still offers both, and replies that never steered by default
(another thread, an idle or queued agent) are unaffected. Saved on this
device next to the sticky comment bar.

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

* fix(server): save one comment per send and honor "don't start" on replay (MUL-7631)

Idempotency now lives on the comment, not the delivery receipt. A send's
client_request_id is stored on the comment behind a partial unique index
(migrations 550/551), so a retry, or a concurrent twin, returns the saved
comment (200) whether the send steered a running turn or fell back to a
normal trigger. Before, a twin could save a second comment, and a send whose
turn had ended kept no receipt to find.

The agents an author chose not to start are stored on the comment too, and
completion replay skips them. "Send as a new run" for one missed recipient,
and "Won't start this time", no longer wake the other agent once its current
run completes. An edit re-records the choice.

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

* fix(issues): repeat the original steering request on retry (MUL-7631)

Until a send is settled, retrying the same text sends its first steering
request unchanged, even after the chosen turn ended. The server then returns
the comment the first attempt saved instead of posting it again as new input.

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

* fix(migrations): fail fast on locks in 549 and 550 (MUL-7631)

Both take an ACCESS EXCLUSIVE lock on a table the product touches
continuously: comment for every read and write, task_supplement for every
daemon claim. Bound lock acquisition and execution, as 483 does for issue,
so a long transaction makes the migration fail and retry on the next run
instead of queueing all traffic behind it.

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

* fix(server): steer at most one turn per comment during rollout (MUL-7631)

Steering is live, and servers from before per-run receipts claim every
receipt of a comment at once. While they still serve daemons during a
rollout, a comment bound to two turns could have the second copy marked in
flight and never delivered. Until every server claims per (comment, run),
a comment steers the first chosen turn only; later recipients keep their
normal trigger. A follow-up lifts the limit after this ships.

Also covers a steer_task_ids entry that names another issue's turn.

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

* fix(server): finish a saved send on retry; keep an edit's choices with its text (MUL-7631)

A send's comment was saved before its agents were started or steered, and
deduplication treated the saved comment as the whole send. A dropped
connection cancelled the request between the two, and the retry only
returned the comment, so no agent ever received it.

- Attempts of one logical send now run one at a time under a transaction
  advisory lock, held on its own connection: a twin waits for the attempt in
  progress (up to 30s, then 409) instead of racing it, and a process that
  dies releases it.
- After the comment is saved, dispatch runs on a non-cancelable context, and
  comment.client_request_dispatched_at (migration 552) records that it
  finished. A retry that finds a saved but undispatched comment finishes the
  dispatch with the comment's recorded choices.

An edit's "don't start" choices were written by a separate statement after
the text, so interleaved edits could leave an older edit's choice. They are
now part of UpdateComment, under the same revision check as the text.

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

* fix(issues): offer one steered recipient per message during rollout (MUL-7631)

The server steers at most one turn per comment until #8800, but the
composer showed "Add to current run" for every running recipient and the
server quietly queued the rest. Now only one recipient steers: the one the
author picked, otherwise the first that would by default. The others show
"Start after this run", and picking "Add to current run" on one of them
moves the steer there.

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

* fix(server): claim a send before dispatching it; drop the request lock (MUL-7631)

The previous recovery marked a send dispatched only after reaching its
agents, so a failure after dispatch made a retry reach them all again. It
also held a pooled connection per send for an advisory lock while the send
needed more from the same pool, which could wait on itself until timeout.

A send's comment now holds a dispatch claim instead
(comment.client_request_dispatched_at, one conditional update): only the
attempt whose claim takes effect publishes the comment and reaches its
agents. A failed claim writes nothing else and returns 500 for the client
to retry; a retry that finds an unclaimed comment claims and dispatches
it; a claimed one is only returned. Concurrent attempts no longer wait on
each other, and post-save dispatch keeps a 30s bound of its own.

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

* feat(server): let one comment steer several running turns (MUL-7631)

Lifts the rollout limit from #8760. Every server now claims receipts per
(comment, run), so a comment can be bound to each running turn its author
chose, and each recipient receives it in its own run.

Merge only after #8760 is deployed everywhere.

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

* feat(issues): steer every running recipient a message addresses (MUL-7631)

The client side of lifting the rollout limit: each running recipient can
take the message in its current turn again, instead of only one per
message.

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

* refactor(issues): drop send-retry idempotency and two display extras (MUL-7631)

A steering send now behaves like any other comment when its response is
lost: a retry posts it again. The machinery that tried to make that retry
return the saved comment grew with every edge it met, and ordinary comments
never had it:

- Remove comment.client_request_id, its unique index, and the dispatch
  claim (migrations 551/552, and 550 reduced to suppressed_agent_ids).
- Remove the replay/resume path in CreateComment, the post-save
  non-cancelable dispatch context, and the client's sticky request id.
  Steering sends only `steer_task_ids`; each binding uses the comment id
  as its receipt request id.

Two display extras go too, since the UI already shows the same thing:

- The "run ended while you were writing" notice: the recipient chip
  already switches from "Add to current run" to its new action.
- "after step N" on a read receipt: the run trace already places the
  message at the step where it was read.

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

* fix(issues): space agent names in Chinese copy; neutral recipients header (MUL-7631)

- The steer badge joined names with Intl.ListFormat, which gives
  "Lambda和Orion" in Chinese. Latin names are now spaced from the Chinese
  joiner ("Lambda 和 Orion"), per the zh copy conventions; the enumeration
  comma stays unspaced.
- The multi-recipient popover was still titled "Will start when sent" and
  its tooltip said "choose who runs", though each row now picks how that
  agent handles the message. Title it "Recipients" and reword the tooltip.

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

* fix(issues): translate the recipient source labels in Chinese (MUL-7631)

"assignee" and "@mention" were left in English in zh-Hans; the recipient
menu header shows them. Use 负责人 (per the zh conventions) and @提及.

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>
2026-09-25 04:42:26 +08:00
mrlonely 79b9327837 fix(issue): bind update attachments safely (#8588) (#8766) 2026-09-24 17:37:32 +08:00
Bohan Jiangandmultica-agent ff8b285497 fix(runtimes): price Claude Opus 5.5 at its own tier (#8808)
Opus 5.5 had no pricing row. The dashboard resolver found no match for
`claude-opus-5-5` and showed its spend as $0, while the backend's
unanchored `claude-opus-5` rule matched it and billed it at Opus 5's
5/25 tier.

Add Opus 5.5 at Anthropic's published rates ($4 input / $20 output /
$5 5m cache write / $0.20 cache read, 0.05x input) to both tables, and
end the backend Opus 5 rule at `claudeVersionEnd`, the same way the
Fable 5 / 5.1 pair is split, so neither Opus rule can take the other's
ids.

Co-authored-by: multica-agent <github@multica.ai>
2026-09-24 17:24:14 +08:00
Kevin Kwok ec70b224d7 MUL-7675: feat(grok): support live-run steering via Grok Build ACP (#8796)
* feat(grok): support steering active Grok Build runs

* test(grok): cover steering before first agent output

* fix(grok): bound interject acknowledgement time
2026-09-24 16:43:10 +08:00
e909e9c89d MUL-7587 fix(daemon): show on-behalf-of identity in agent context (#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>
2026-09-24 15:35:54 +08:00
bojackli df2e86cf3a fix(daemon): retry Windows task root record rename (#8786)
Refs #8785
2026-09-24 15:10:44 +08:00
Bohan Jiangandmultica-agent 0da0b46bbf MUL-7672: only a closing keyword completes an issue on PR merge (#8794)
* feat(pr): only a closing keyword completes an issue on merge (MUL-7672)

#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 #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 #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 #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>
2026-09-24 14:56:59 +08:00
Bohan Jiangandmultica-agent 9c69661f7f feat(web): licensing FAQ, privacy policy, About team section, source-available wording (MUL-7558) (#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>
2026-09-24 14:25:49 +08:00
54520de957 MUL-7646: fix(claude): report per-run usage for resumed sessions (#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>
2026-09-24 13:44:51 +08:00
1a09c65b1a feat(llm): support MULTICA_LLM_DISABLE_THINKING env (MUL-7162) (#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>
2026-09-24 13:07:25 +08:00
Bohan Jiangandmultica-agent 4f5fbc9216 fix(vcs): explain untrusted TLS certificates and trust private CAs via Helm (MUL-7639) (#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>
2026-09-24 13:04:01 +08:00
YYClaw 4f315a2714 fix(dingtalk): avoid unnecessary issue link escapes (#8768) 2026-09-24 12:58:29 +08:00
pseudoyuandClaude Fable 5.1 62efe26aa0 MUL-7620 feat(telegram): send and receive photos, videos and files (#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 #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 #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>
2026-09-24 12:41:47 +08:00
9df0cae78a MUL-7429: complete issues when every linked PR merges (#8758)
* 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>
2026-09-24 02:58:19 +08:00
mrlonely 0516dd57c3 MUL-7625: feat(antigravity): stream live tool execution events (#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
2026-09-23 19:02:44 +08:00
d45aba1cd7 fix(tasks): settle supplements in application transactions (MUL-7630) (#8755)
Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-23 18:02:59 +08:00
4469bac632 fix(issues): duplicate mark follow-ups from #8741 review (MUL-7349) (#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>
2026-09-23 17:41:27 +08:00
Yincan Huangandmultica-agent 7be175346e MUL-7613: fix(agent): refresh live model catalogs (#8722)
* 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>
2026-09-23 16:57:36 +08:00
Bohan Jiang 1539c54ec5 feat(issues): make duplicate marks traceable and one click from the original (MUL-7349) (#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.
2026-09-23 16:55:50 +08:00
Bohan Jiangandmultica-agent 8355c7aeca fix(cli): name the TLS handshake timeout and stop masking it on login (MUL-7572, GH #8654) (#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>
2026-09-23 16:45:36 +08:00
93ae552ba9 feat: add messages to active Claude Code and Codex runs (MUL-7565) (#8682)
* feat: add live run supplements (MUL-7565)

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

* fix: address MUL-7565 review feedback

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

* fix: resolve MUL-7565 supplement panel and receipt review

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

* refactor: remove MUL-7565 supplement ordinals

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

* fix: close supplement composer after run remount

* fix: exclude task supplements from completion reconciliation

* fix: build a persistent binary for local daemon launches

* feat: support Claude task supplements through SDK hooks

* refactor: simplify task supplement delivery

* test: remove redundant task supplement coverage

* fix: remove redundant supplement delivery notices

* refactor: extract local daemon launcher fix to its own PR

* fix: preserve ordinary comment size compatibility

* fix: accept empty task start acknowledgements

* fix: bound task start response decoding and retries

---------

Co-authored-by: Forge-Boy <forge-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: yushen <ldnvnbl@gmail.com>
2026-09-23 16:10:07 +08:00
Bohan Jiangandmultica-agent 9b8da372cc MUL-7450 fix(lark): surface the event-delivery check a silent Bot needs (#8735)
* fix(lark): surface the event-delivery check a silent Bot needs

A Feishu app whose event subscription delivers to a request URL instead
of the long connection binds successfully, shows Connected everywhere,
and never receives a message. Multica has no webhook ingress, so those
events go nowhere near us. Nothing in the product said so: the drop path
in the connector writes no log line, an inbound audit row is only reached
once a message enters the Router, and the troubleshooting docs listed
only the causes this case is not.

Three pointers, no behaviour change:

- The connector logs the event type when the decoder declines a frame,
  so a socket that is up but only receiving events we do not handle is
  distinguishable from a healthy one. Heartbeats carry no event type and
  stay silent, so the log does not fill with noise.
- The connected badge on the agent's Integrations tab names the long
  connection and links to the guide. That is where someone looks when
  the Bot stays quiet; the install dialog closes itself on success.
- The integration guide's troubleshooting list gains the subscription
  mode and event subscription, in all four locales.

Refs #8496

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

* fix(lark): report a dropped event type once per connection

Review on #8735: one Info line per declined frame is unbounded. An app
that subscribes to reactions or membership churn delivers those
continuously, and there are thousands of Feishu installations.

Report each event type once per connection instead. The map is
read-loop-local, so it needs no lock and a reconnect re-reports. What
the line is worth stays the same: it names what IS arriving on a socket
whose drops leave no other trace. It says nothing about what is missing
— an app delivering to a request URL sends no frames at all, so it
produces no line, which is why install-time verification is the actual
fix for #8496 rather than this.

Also drops apps/web/next-env.d.ts from the branch: Next regenerated it
during a typecheck run and it has nothing to do with this change.

Refs #8496

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-09-23 15:25:45 +08:00
Bohan Jiangandmultica-agent 6ee01c33aa fix(cli): reference issue create attachments in the description (#8736)
`multica issue create --attachment` uploaded each file after the issue
existed, stamping it with the new issue_id and leaving the description
untouched. An issue renders only the files its description references, so
the file was stored and reachable through the API while appearing nowhere
on web, desktop or mobile.

Upload the files first, unbound, append each one's markdown to the
description (images inline, other files as a card), and bind every id via
attachment_ids on the create call. Files already referenced by the
description are not appended again, which keeps the quick-create path —
where the user's pasted markdown is already in the body — from rendering
the same file twice.

Uploading first also drops the old partial-success state: a failed upload
now means no issue was created, so the retry cannot duplicate one.

Co-authored-by: multica-agent <github@multica.ai>
2026-09-23 15:19:26 +08:00
96db419c0a feat: mark an issue as a duplicate from the status picker (MUL-7349) (#8728)
Enable step. The previous release added duplicate_of_issue_id and the
code that keeps it correct; this one lets people create a mark.

PUT /api/issues/{id} accepts a write-only duplicate_of_issue_id, which
sets status to cancelled and so reuses every status side effect
(status_changed event, stage barrier, wakeups). Marks are validated
under row locks on both issues: the target may not itself be a duplicate
and an issue with duplicates may not be marked, so the relation stays
one level deep and concurrent cross-marks cannot form a cycle.
GET /api/issues/{id}/duplicates returns both sides, and applies the
validity rule itself so a rollback past the previous release still reads
correctly.

The status picker on an existing issue gains a 'Mark as duplicate...'
action in its footer, outside the arrow-key listbox so it is never
mistaken for a status. A duplicate shows a banner linking to its
original with an unmark action; the original lists its duplicates.
Copy added for en, zh-Hans, ja, ko and fr.

Co-authored-by: J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-23 15:18:27 +08:00
YYClaw be085b1222 fix(chat): avoid loading transcripts during cancellation (#8708) 2026-09-23 15:00:20 +08:00
b7cdd76ad8 MUL-7349: feat(server): add the duplicate-mark column and keep it correct (1/2) (#8691)
* feat(server): add the duplicate-mark column and keep it correct (MUL-7349)

Expand step for marking an issue as a duplicate of another. A duplicate
is an ordinary cancelled issue that records which issue it duplicates;
there is no duplicate status. This release adds the column and the code
that maintains it, and nothing writes a pointer yet.

- A mark survives only a write that leaves an already cancelled issue
  cancelled. Every other write drops it, so reopening removes a mark and
  cancelling again does not bring one back. Both status-writing queries
  carry the rule, which covers the UI, batch updates and background
  writers.
- Deleting an issue clears the pointers of its duplicates in the same
  transaction, the way deleting a parent detaches its children.

Shipping this before marking is what makes marking safe to roll back: the
release a rollback lands on already clears the pointer. Tests seed a mark
the way the next release will write it.

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

* fix(server): compute issue updates from the locked row (MUL-7349)

UpdateIssue built its next_* values in a CTE that read the row before
the UPDATE took its row lock. A write that waited there behind another
transaction then resumed with the values it had read before the wait and
wrote them back, so an ordinary priority or status edit could restore a
duplicate mark the other transaction had just cleared — by reopening the
issue or by deleting its original — and did_change/did_activity were
decided against the pre-wait row as well.

Lock the row in the CTE instead, so every computed value comes from the
row the statement is about to write. Tests hold the clearing transaction
open and wait for the update to block on the row lock, so the
interleaving is deterministic; both fail without this change.

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

---------

Co-authored-by: J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-23 14:59:47 +08:00
YYClaw 42727f697e MUL-7611: fix(comments): read only thread routing owners (#8712)
Comment owner routing loaded every task row on an issue, including context/result payloads, before selecting the current thread owners. Filter by issue and root comment in SQL and return one agent/squad pair per owner.

Closes #8711
2026-09-23 14:39:36 +08:00
Bohan Jiangandmultica-agent 81f0fb418a test(execenv): stop the test repo template racing git auto maintenance (#8730)
The shared template repo is built once per test binary and handed to every
test as an os.CopyFS copy, so nothing may write inside it after
buildTestRepoTemplate returns. git commit breaks that: it forks a detached
`git maintenance run --auto` and returns without waiting, leaving a
background process that creates and removes lock files under .git/.

CopyFS then lists .git/objects/maintenance.lock and fails to open it once
maintenance has removed it, failing whichever test happened to call
newTestRepo at that moment — most recently
TestGitRootLockTimeoutDoesNotAdviseDeletingTheLock on an auth-only PR
(#8706), which touches nothing in this package.

Disable auto maintenance in the template's config. Copies inherit it, so the
git commands tests run against their own copy cannot spawn a background
process that races their assertions either.

Co-authored-by: multica-agent <github@multica.ai>
2026-09-23 14:38:02 +08:00
Francisco Guimarães (chico)andmultica-agent 5403083420 MUL-7607 fix(auth): restore invitation-based signup (#8706)
Co-authored-by: multica-agent <github@multica.ai>
2026-09-23 14:33:43 +08:00
hutiefang76 1879bc35e3 MUL-7351: fix(daemon): fence and retry task start acknowledgements (LANX-3) (#8400)
* fix(daemon): recover quick-create writes and task start acknowledgements

* fix(daemon): fence task start retries by claim generation

* fix(daemon): align start claims with main and canonical UTC
2026-09-23 14:25:57 +08:00
pseudoyuandClaude Fable 5.1 d31fcbc9b7 MUL-7585 feat(telegram): inline recent group context on @mentions (#8673)
* feat(telegram): inline recent group context on @mentions

When a member @-mentions the bot in a group, the agent only ever saw that
single line. Lark solves this with a <recent_context> prefetch
(lark/inbound_enricher.go, MUL-3084); Telegram could not copy it because
the Bot API has no history endpoint — getUpdates is consume-once.

Each installation's polling loop now keeps an in-memory ring of the last
DefaultRecentContextSize (10) human group messages per chat and per forum
topic, and inlines that window as a <recent_context> block ahead of an
addressed group message, in the same recent → quoted → own composition
Lark uses. The trigger and an explicitly quoted parent are excluded from
the window; /new starts a chat without ambient context; p2p chats and
unaddressed messages are never enriched. Unaddressed chatter is still
never persisted or turned into a session (MUL-2671): it only surfaces as
read-context on a turn a member directed at the bot, and the ring dies
with the polling loop.

The docs now say what the window contains and that Group Privacy must be
off in @BotFather for the bot to receive the surrounding messages at all.

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

* docs(telegram): state the recent-context retention and privacy contract accurately

Review follow-up for #8673. Setup step 4 now presents Group Privacy on as
the default, minimal-visibility choice and turning it off as the optional
step that enables recent group context, with its cost spelled out. The
Groups section no longer claims unaddressed chatter is "never stored": it
says the message is held in a short in-memory buffer and saved with any
addressed turn that pulls it in, that a reply to the bot triggers the
context just like an @-mention, what Telegram delivers under each privacy
setting, and that an admin bot receives everything regardless. Same fix in
zh/ja/ko. The two code comments that relied on privacy mode being on, or
claimed the message was never persisted, now describe the real contract.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 14:22:48 +08:00
90e0bdf830 MUL-7586 Atomically create issues with custom properties (#8689)
* feat(issues): create with properties atomically

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

* fix(issues): recover unavailable properties in create drafts

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

* test(issues): isolate commit failure task assertions

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

---------

Co-authored-by: Forge-Boy <forge-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-22 16:44:14 +08:00
importcpp 2b60f3b5de MUL-7594: fix(server): record creation activity for autopilot issues (#8684)
Autopilot and channel-created issues publish issue:created with a map payload; the activity listener only accepted the HTTP IssueResponse struct and silently dropped the creation activity. Reuse the existing extractIssueFields normalizer so both payload shapes record and broadcast the created entry.
2026-09-22 16:28:28 +08:00
d2423aa35c MUL-7590: fix(execenv): support OpenClaw agents.entries per-agent workspaces (#8678)
* fix(execenv): pin per-agent workspaces under the OpenClaw agents.entries schema

OpenClaw 2026.8 retired the agents.list config path and reports it as
"Unknown config path: agents.list", moving the agents to a map keyed by agent
id under agents.entries. The per-task wrapper only ever wrote
agents.defaults.workspace, and an agents.entries.<id>.workspace entry outranks
it verbatim: on a host where adding a second agent pinned the gateway agent to
an absolute workspace, every task ran in that pinned directory instead of its
prepared workdir, so the workdir skills/ never loaded and the agent edited the
wrong tree.

Read agents.entries when agents.list is gone, pin every entry workspace to the
task workdir on write-back, and keep the registry fallback for the generations
that have neither config path. The stdout-envelope branch now matches the
path-aware wording, so a 2026.8 host that prints its envelope without exiting is
read instead of failed closed, and the entries probe shares the agents.list
deadline so the worst case stays three CLI deadlines.

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

* test(execenv): cover the agents.entries probe, write-back and shared deadline

Adds the regressions the review of the earlier attempt asked for: the current
"Unknown config path" envelope with err == nil selects the entries probe and
never the registry, both the plain and the managed-MCP flows pin every
agents.entries.<id>.workspace to the task workdir, a valid-but-unset entries
path falls back to the registry, and an envelope naming an unrelated path still
fails closed.

The worst-case budget test now drives the five-invocation chain and pins the
config-schema pair to one shared deadline, so a fourth budget fails loudly.

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

* fix(execenv): pin agents.entries workspaces without copying the entry

Review feedback on the write-back: `config get` redacts the config before it
returns the path it was asked for, so every sensitive per-agent field comes back
as `__OPENCLAW_REDACTED__`. The wrapper is a sibling of the user's config in the
$include merge and siblings win, so copying the resolved entry would write that
sentinel over the user's real secret — memory search API key, web fetch headers,
sandbox SSH credentials — for every task on the host, and nothing on OpenClaw's
load path restores it. `agents.entries` is an object keyed by id and objects
merge recursively, so `{"<id>": {"workspace": "<workdir>"}}` is enough to pin the
workspace while every other field still arrives from the user's own config.

The same reasoning drops the resolved payload from the row representation: an
entries row now carries the id marker alone, so no redacted copy of the user's
agent config is held in memory or written to the discovery cache file.

Since only the source still decides the write-back, it is now carried
explicitly (`openclawAgentsSource`: list / entries / registry) rather than as a
boolean that effectively meant "not agents.list", and the source is recorded in
the cache entry so a hit rebuilds the same wrapper. The cache shape version is
bumped, so an entry written by an older daemon is a clean miss.

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

* test(execenv): assert the entries write-back emits workspace alone

Review feedback: the tests encoded the old contract (name / model carried into
the wrapper verbatim). They now assert the wrapper carries exactly one key per
entry — `workspace` — and that nothing else from the resolved payload reaches
the file OpenClaw reads.

The new regression drives the redaction shapes upstream's own redact-snapshot
suite covers (memory search apiKey, web fetch headers, sandbox SSH credentials)
through the entries probe of a real preparation, and fails if any of them ends
up in the wrapper: with the copying write-back restored it reports the sentinel
in `agents.entries.main`. The unit test for the write-back gate also covers a
half-marked list, which a quiet per-row filter would have pinned anyway.

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

---------

Co-authored-by: tianshuai1 <tianshuai1@huya.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-22 16:22:28 +08:00
8f1ad46ee8 MUL-7556: index GitHub PR address lookups (#8636)
* perf(db): index GitHub PR address lookups (MUL-7556)

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

* perf(db): narrow GitHub PR address index (MUL-7556)

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

* fix(migrations): renumber GitHub PR address index

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

---------

Co-authored-by: Forge-Boy <forge-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-09-22 15:33:23 +08:00
af62622e50 MUL-7288: fix(server): wake guest squad leaders on worker replies (#8314)
* fix(server): wake guest squad leaders on worker replies

* fix(server): ignore tombstoned guest delegations

* fix(server): preserve assigned squad routing under tombstones

* test(server): narrow guest squad routing coverage

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

* test(server): cover member-assigned guest squads

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>
2026-09-22 14:26:27 +08:00
Multica Eveandmultica-agent 5140b99d86 MUL-7577: remove no-start guidance from runtime and platform skill (#8680)
* fix(prompts): remove no-start guidance from runtime briefs

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

* fix(skills): remove no-start guidance from platform instructions

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-09-22 13:27:52 +08:00
Xichang(Seacen) ZhaoandJ da139a7c97 fix(wecom): name the two exits that leave a reply unaccounted for (MUL-7318) (#8348)
The WeCom outbound path had two silent exits: a completion whose delivery row
named another platform, and one with no delivery row at all. From outside they
were one quiet return, so an answer that reached the transcript but never the
chat could not be told apart from ordinary traffic on the shared bus.

Both now name themselves in the closed outcome set: not_wecom_turn (DEBUG),
no_delivery_row (WARN, the one skip worth an alert — in practice a task
enqueued before an upgrade), and route_unattributable (DEBUG) for a turn with
no route and no batch owner to attribute it to. The metrics Help and logs draw
the drop/skip line by obligation rather than by whether a frame was written,
and tests pin that wording.

The origin gate is one keyed read (GetTaskChannelOrigin) in place of
GetAgentTask plus TaskInputIsChannelIngested. It also reports whether the
batch owner is known, so an owner-less row is delivered when a route exists
and stays quiet when none does — which keeps the auto-retry of a legacy
channel turn answering its room.

Co-authored-by: J <bohan@devv.ai>
2026-09-22 13:23:25 +08:00
Bohan Jiangandmultica-agent f41fae6b08 fix(cursor): own Windows background shells by launch Job membership (MUL-7417) (#8638)
* fix(cursor): own Windows background shells by launch Job membership (MUL-7417)

captureCursorBackgroundProcess on Windows walked a machine-wide PID->PPID
snapshot to prove a reported shell descends from the launch and to find
its pre-existing children. Windows keeps a child's recorded PPID after
the parent exits and reuses the PID, so the walk could reach a process
owned by someone else; opening it failed with 'Access is denied' or
'not an owned descendant' and the whole capture aborted. The daemon then
fell back to the idle watchdog and a Cursor run that later went quiet
took the 10-minute watchdog path instead of completing. In CI this was
TestCursorBackgroundLifecycle/multiple failing about 2 runs in 43.

Take candidates from the launch's own Job Object instead
(QueryInformationJobObject / JobObjectBasicProcessIdList). Descendants
inherit the Job at creation and cannot leave it, so membership is the
ownership proof: the parent-chain walk goes away, the PPID snapshot is
used only to attach a member's pre-existing children, and a member that
exits or is reused between listing and open is skipped instead of
failing the capture. Creation ordering still tells a real child from
one that points at a reused parent PID.

Restore TestCaptureCursorBackgroundLateDescendant on Windows; it was
excluded for exactly this race.

ci.yml runs the Windows Cursor step with -count=20 on this PR only to
show the race is gone; it is reverted before merge.

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

* ci: restore -count=1 on the Windows Cursor background step

The -count=20 stress run on this PR passed (run 35580440594, windows-execenv);
the per-merge step goes back to a single pass.

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

* fix(cursor): complete the Job member list and refuse unopenable live members (MUL-7417)

Review follow-ups on #8638:

- cursorJobProcessIDs grew its buffer only on ERROR_MORE_DATA. The kernel
  can also return success with NumberOfProcessIdsInList short of
  NumberOfAssignedProcesses, which a Job over the initial capacity or one
  growing during the query would hit; the short list then passed for the
  membership and could reject the target or leave children out. Retry
  until the counts agree, fail explicitly if they never do, and make the
  initial capacity a package var so a three-process tree exercises the
  growth path in a test.

- The child walk skipped every open failure as 'exited or reused'. A live
  member whose DACL denies PROCESS_SET_QUOTA|PROCESS_TERMINATE also fails
  to open, and skipping it left the capture reporting success with that
  member outside the tool Job. Skip only PIDs proven to carry no member
  (handle outside the launch Job, or no longer listed after a re-query,
  confirmed by one more open); otherwise fail the capture so the caller
  takes its existing watchdog fallback. OpenProcess goes through a
  package var so a test can make a live member unopenable.

Windows-only tests cover both, plus the plain claim-and-terminate path
for a shell with a pre-existing child.

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

* test(cursor): name the Job list growth test so the Windows CI filter runs it

The windows-execenv step selects 'TestCursorBackground|TestCaptureCursorBackground';
TestCursorJobProcessIDsGrowsPastInitialCapacity matched neither and never ran.

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

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-09-21 18:36:34 +08:00