Commit Graph
844 Commits
Author SHA1 Message Date
Jagan Fulare a8e68a1b09 feat(admin): expose market catalog 2026-10-31 10:12:06 +00:00
Jagan Fulare 1060977bc6 feat(admin): create workspaces 2026-10-31 10:05:33 +00:00
Jagan Fulare 4297bf54ca fix(auth): block machine token minting 2026-10-31 10:05:33 +00:00
Claude d1978ec404 chore(migrations): renumber platform admin migrations to 600-606
main now carries 583 and 584, so the admin console migrations move past
the numbers reserved by the open-connector work (590-599).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FrVjAFWeckYV69HS51xUhy
2026-10-31 10:05:29 +00:00
Claude 396752349f feat(server): platform admin API with audit log and DB-backed disabled users
Adds the backend for the operations console: platform_admin roles
(super_admin / operator / viewer), an admin_audit_log table, and
user.disabled_at so disabling an account is a stored state instead of a
hard-coded denylist. The in-process disabled set is now refreshed from the
database every 30 seconds, and the old emergency denylist entries are
migrated into it.

/api/admin exposes overview counts, user and workspace listings with
detail, disable/enable, token revocation, the audit log, and platform
role management. Every write requires a reason and is recorded with the
actor, IP and user agent. PLATFORM_ADMIN_EMAILS bootstraps the first
super_admin.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FrVjAFWeckYV69HS51xUhy
2026-10-31 10:05:25 +00:00
Cursor Agentandklinsmaya 426ac89695 feat(market): serve install operations under /api/market
Commit, read, and reconcile share the install ledger. The watcher leases
due operations. A missing catalog policy pin writes no row, and workspace
delete removes the new tables.

Co-authored-by: klinsmaya <[email protected]>
2026-10-18 09:40:25 +00:00
Cursor Agentandklinsmaya 5145706b07 chore(market): drop comments that restate the code
Two comments described rules the types do not enforce. ParseListingID is not the only way to build a ListingID, and decodeVersion returns more than ErrInvalidListing.

Co-authored-by: klinsmaya <[email protected]>
2026-10-18 04:48:50 +00:00
Cursor Agentandklinsmaya 4873a8690e test(market): reject task tokens at the market routes
The sketch's router test plan named task-token rejection. A member who gets
200 with a session gets 403 from RequireHumanActor with a task token bound
to them, on browse, detail, and preflight.

Co-authored-by: klinsmaya <[email protected]>
2026-10-18 04:13:49 +00:00
Cursor Agentandklinsmaya 2deff33309 feat(market): serve browse, detail, and preflight
Mount /api/market inside the workspace-member group behind market_v1 and
RequireHumanActor. Preflight takes version, runtime_id, and model only;
the member comes from the workspace middleware. Configure the catalog
with MULTICA_MARKET_CATALOG_URL and MULTICA_MARKET_CATALOG_TOKEN; an
empty URL serves the stub samples.

Co-authored-by: klinsmaya <[email protected]>
2026-10-18 03:41:20 +00:00
da1671ca20 MUL-7420: Project workflows — columns and handoffs on the workflow (#8814)
* feat(workflows): workspace workflows with step handoffs (MUL-7420)

A workflow is a named, workspace-level selection of statuses from the
shared status catalog, in board order, with an optional handoff on each
step. Projects opt in through project.workflow_id; a project without one
keeps the whole catalog (the implicit Default workflow), so existing
workspaces see no change.

- issue_workflow table + project.workflow_id, CRUD API under
  /api/issue-workflows, POST /api/projects/{id}/workflow with a dry-run
  status mapping plan. Only issues on statuses the target workflow does
  not list are remapped, and remapping never hands issues off.
- Issue writes keep the status inside the project's workflow. Entering a
  step with a handler through a status change assigns the issue to that
  handler and starts its run with the step's brief as the handoff note;
  an explicit assignee in the same write wins. Creating an issue starts
  at the workflow's starting step.
- Task claims carry the issue's project workflow, and the brief renders
  a Project Workflow section so agents move along this project's
  statuses instead of the built-in ones.
- Statuses a workflow lists cannot be archived.
- Adoption is gated by the project_workflows_v1 flag; existing
  workflows keep working and projects can always switch back to Default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(workflows): workflow settings, project picker and workflow-aware boards (MUL-7420)

- Settings → Workflows (behind project_workflows_v1): the Default
  workflow plus named workflows, each shown as its step chain with
  handlers and the projects using it; an editor to pick statuses from the
  library, order them, mark the starting step, and set each step's
  handoff, instructions and next/back statuses.
- Project Workflow property: switching dry-runs first and asks where
  issues on unlisted statuses go; nothing else moves and nothing is
  handed off.
- A project board's columns are its workflow's steps, and each column
  head names who entering it hands the issue to.
- Status pickers offer only the issue's workflow steps and say who each
  hands off to; boards refuse drops onto statuses the dragged card's
  workflow does not list.
- New issues in a workflow project start on its starting step, with a
  hint naming who they will be handed to.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(workflows): name built-in steps once in the brief; roomier picker hints (MUL-7420)

Built-in statuses have no stored name, so the handoff brief printed
`todo` todo. Print the key alone when the name adds nothing.

The status picker widens for workflow projects and shows the handoff as
an arrow and name, so step names no longer truncate to one letter.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(workflows): handoff previews and a single timeline entry per handoff (MUL-7420)

- GET /api/issues/{id}/workflow-handoff?status= says what moving an issue
  would do: who it goes to, which runs of its current agent are still
  active, and the brief the handler receives. Backs the confirmation.
- POST /api/issue-workflows/preview-brief renders a step's brief from an
  unsaved workflow, for the editor's preview.
- Mapping plans list the statuses that stay, so the switch dialog can say
  which issues are untouched.
- A handoff is recorded on the status_changed activity (workflow, handler,
  whether a run started, the brief) instead of a separate assignee row.
- The brief now reads like the one shown in the design: the workflow, the
  current step, its instructions, the statuses, and the commands to hand
  off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(workflows): rebuild the workflow UI to the reviewed design (MUL-7420)

The first cut used generic controls where the design had purpose-built
ones. This brings every surface up to the mockup:

- Settings → Workflows is one page: the workflows (step chain with
  handler avatars, handoff count, projects using it; Default first) and,
  below, the shared status library with each status's workflow usage.
- The workflow editor is a full page instead of a dialog: draggable step
  cards summarising each handoff, and a panel for the selected step —
  swap its status, pick the handler with a segmented control and a
  searchable target, handoff instructions, next/back statuses, a preview
  of the brief the handler receives, duplicate and delete.
- The project Workflow picker lists each workflow's statuses, handoffs and
  projects, links to settings, and always confirms the switch with what
  moves and what stays.
- Moving an issue into a handoff step (status picker, table, board drop)
  asks first: who it goes from and to, a default-on option to stop the
  previous agent's run, and the brief.
- Status pickers name the workflow and project, show each step's handler
  with an avatar or that the assignee stays, and explain themselves.
- Boards: the handler chip in project column heads, hatched columns a
  dragged card cannot enter with the reason, and a drop hint naming who
  the card will go to.
- The timeline records a handoff under its status change, with the brief
  the run received; the issue sidebar shows the workflow it follows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(workflows): word a queued previous run as queued in the handoff confirm

The confirm dialog said the previous agent was "still running (for N
minutes)" even when its task had only been queued. Queued runs now read
as queued, and the checkbox cancels them instead of stopping them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(workflows): refuse a run's status change once the issue left its step

A run decides where the issue goes next from the step it was queued for.
When someone else moved the issue while the run worked, applying that
decision undid their move: in the live Grok run, a PM parked NW-1 as
blocked and the still-running agent moved it straight back to code
review.

Tasks now record the issue's status when they are queued
(agent_task_queue.workflow_step, written by the task INSERTs; automatic
retries keep their parent's step), and a run's own status changes move
it along. In a workflow project, an agent status change from a run whose
step no longer matches the issue is refused with 409
workflow_step_moved, naming where the issue is now and who moved it, so
the agent stops instead of retrying.

A handoff away from a squad now also stops the runs made on the squad's
behalf, and stopped runs record who stopped them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(workflows): keep every status entry point inside the issue's workflow

The issue context menu listed every catalog status and applied handoff
steps without the confirmation the status picker and board show, so a
right-click could pick a status the workflow refuses or start an agent
unasked. It now offers the workflow's steps and opens the handoff
confirmation through a shared modal.

One-click status actions no longer assume built-in statuses a workflow
may not list: "mark done" lands on the workflow's done step (hidden when
it has none), un-marking a duplicate returns to the workflow's start,
and "mark duplicate" is offered only when the workflow lists cancelled.
The batch toolbar offers a single project's workflow statuses when the
selection sits in one project.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(workflows): keep issue wakeups consistent with workflow steps

Three places where project workflows and issue wakeups met:

- A wakeup run is queued when it fires, so it took the issue's status at
  that moment as its workflow step. A wakeup an agent registered then
  bypassed the stale-step refusal: parked by a PM, the issue could be
  moved on by the agent's "check CI, then hand to review" wakeup. It now
  keeps the registering run's step; a member's wakeup still starts from
  the issue's status.
- Retries carried handoff_note only for wakeup runs, so a retried
  workflow handoff lost its brief. Rows with a workflow step keep it.
- Switching or editing a workflow remaps statuses in bulk without the
  close rule, so an issue remapped into a done status kept its wakeups
  and pending wakeup runs. The remap now ends them like any status write.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): don't also wake the agent a write hands the issue to

A workflow handoff or an assignment already starts the receiving agent's
run. If that agent also subscribed an event wakeup to the issue's status
or assignee, the same write queued a second run for one fact, and
workflows made such writes frequent.

The write now names the agent it hands the issue to in the
transaction-local multica.handed_to_agent (the new agent assignee, a
squad's leader, or a step's handler even when it already owned the
issue), and wakeup capture skips that agent's subscriptions for the
write's issue.* events. Other agents' wakeups, and task, comment and
reaction events, are captured as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(workflows): tell agents not to watch workflow issues with wakeups

The handoff brief and the runtime's project workflow section now say
that each step's handler gets a run when the issue enters the step, so
wakeups on a workflow issue are for waiting on time or on something
outside it, not on its status, assignee or runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(workflows): refresh project usage after project changes

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

* feat(workflows): MUL-7420 split columns from per-project handoffs

Workflows now only define a board's columns between a fixed head
(backlog, todo) and tail (blocked, done, cancelled). Handoffs move to
project.workflow_handoffs, so projects can share columns and still hand
off to different people. Default becomes a real, editable row that every
project without a named workflow follows. Adds project handoff, add-column
(project fork or shared) and project brief preview endpoints; same-agent
adjacent columns re-run the agent.

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

* feat(core): MUL-7420 project handoffs and Default workflow client model

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

* feat(views): MUL-7420 column-header handoffs and project workflow menu

Board column headers set the project's handoff for that column; the
project toolbar gets a workflow menu (full flow, switch, templates) and an
add-column action. Views outside a project show Default's columns, with
other statuses reachable through a status filter. Adds the issue flow card,
create-project workflow choice, a columns-only workflow editor and a
lighter status picker.

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

* fix(views): MUL-7420 wrap create-project workflow label in a menu group

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

* feat(workflows): MUL-7420 keep workflow structure in settings; no squad handoffs

Projects no longer add columns or create workflows: the add-column
endpoint is removed and creating a workflow is admin-only. Handoffs go
to one agent, a member or the project lead; squads route work themselves,
so a handoff to one is rejected, as is the issue-creator handler.

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

* refactor(core): MUL-7420 drop the project add-column client

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

* feat(views): MUL-7420 simpler handoff picker and workflow editor form

The handoff form is one searchable "Hand to" picker (no handoff, the
project lead, an agent or a member); the next status defaults to the next
column and next/back overrides sit under "More options". The workflow
editor gets labelled name and description fields and shows the fixed
statuses as one start and one end row. Templates move from the project
menu to Settings → Workflows → New, and the project board loses its
add-column action.

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

* refactor(server): MUL-7420 keep handoffs on the workflow, make backlog and blocked optional

Handoffs move back onto workflow steps: a workflow is the whole pipeline,
and a project only picks one. The project lead stays a relative handler, so
projects can still share a workflow. Handoff edits are admin-only, through
the workflow write or PUT /api/issue-workflows/{id}/handoffs/{status}; the
brief preview moves to the workflow too. Drops the unreleased
project.workflow_handoffs migration.

Todo, done and cancelled are required; backlog and blocked are optional and
keep their fixed places. Without blocked, the brief points agents at the
step's back status.

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

* feat(views): MUL-7420 edit handoffs on the workflow, optional backlog and blocked

The workflow editor sets each column's handoff in its side panel, and
Backlog and Blocked switch on and off. The board's column popover and the
flow panel edit the workflow's handoff, say how many projects it reaches,
and are read-only for non-admins. Drops the per-project handoff client
code.

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

* fix(wakeups): MUL-7420 keep platform rules on writes that hand off nobody

Migration 579 skips an issue.* event for the agent a write hands the issue
to. Its filter compared agent_id with the transaction's handed_to_agent
setting using IS DISTINCT FROM, which is false when both are NULL. Main's
platform rules (child_done) have no agent_id, so every issue.* event that
handed the issue to nobody was dropped for them: a parent leaving backlog
never woke its agent for sub-issues that closed meanwhile
(TestChildDoneHeldWhileParentInBacklog).

Rules without an agent are never the handoff target, so they always
capture. Extends TestHandoffDoesNotAlsoWakeTheHandler with a platform rule
on a handoff and on a plain status change; the latter fails on the old
filter.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): MUL-7420 three-way handoff choice; quieter move confirm

The handoff form picks one of three directions, as in the mockup: no
handoff, hand to an agent (starts a run), or hand to a member, project lead
included (assignee only). What the step does, where it goes next and the
back status only reach an agent's brief, so they show and save for agent
handoffs alone.

The board's move confirmation keeps the agent's brief collapsed. Create
project shows the workflow picker even when Default is the only workflow,
with a note on where to make more. The hidden-columns panel says a board's
columns come from its workflow, with a link to manage workflows for admins.

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

* feat(workflows): confirm handoff on project move, per-project handoff copy, two-pane editor (MUL-7420)

- A single issue moved into a workflow project from issue detail confirms
  the handoff of the column it lands on; the write opts in with
  enter_workflow_step. Batch, API and CLI moves stay administrative.
- The handoff preview takes project_id to preview such a move.
- Saving a shared workflow's handoff from a board can keep the change to
  that project: the server copies the workflow for it and switches the
  project in one transaction (copy_for_project_id).
- The column popover says which workflow it edits and who else uses it.
- The workflow editor is a step list with a detail panel for the
  selected step.

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

* style: gofmt workflow tests

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

* feat(views): MUL-7420 show next/back always; edit workflow name and description in place

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

* feat(views): MUL-7420 trim copy on the workflows settings page

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

* fix: MUL-7420 keep pre-workflow behavior for workspaces that never adopt workflows

The run step guard now applies only to workflows that hand off, and the
standalone Statuses settings page keeps its original header.

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

* fix(views): MUL-7420 use a solid placeholder tone on the workflow header

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

* fix(workflows): keep the runtime brief cache-stable; fix project board and batch status gaps (MUL-7420)

- Move the project workflow out of the runtime brief into the per-turn
  prompt so CLAUDE.md stays byte-identical across steps and issues.
- An explicit status filter overrides the project workflow's columns; the
  hidden-columns panel lists statuses outside the workflow.
- Batch status offers only statuses every selected workflow allows; a
  partial batch rolls back, refetches and warns.
- setProjectWorkflow returns void instead of an unparsed body.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Lambda <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-10-16 16:49:49 +08:00
86020b88a4 MUL-8047: chore(server): remove the pre-v3 desktop onboarding shim (#9291)
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-16 16:41:46 +08:00
Jiayuan Zhangandmultica-agent 231dedb552 MUL-8034: chore: clean historical comments across the codebase (#9274)
* chore: clean historical comments (MUL-8034)

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

* docs: preserve pending Composio expiry contract

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

---------

Co-authored-by: multica-agent <[email protected]>
2026-10-15 21:54:33 +08:00
d3c1b44d75 MUL-8014: fix(realtime): revoke workspace WebSocket access (#9249)
* fix(realtime): revoke workspace access on membership removal

Close removed members' sockets, revalidate registration, and gate captured write batches with fresh coalesced membership queries. Cover relay failure, replay, queue races, and transactional removal/rollback (MUL-8014).

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

* fix(realtime): keep registration free of database waits

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

* perf(realtime): narrow removal event prefilter

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

* test(wakeup): avoid global backfill in workspace default test

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

* fix(realtime): bound concurrent authorization and defer relay revocation

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

* test(realtime): check caller budget without waiting for expiry

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

* fix(realtime): decouple member-list invalidation from revocation

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

* perf(realtime): skip removal decoding for member actor metadata

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-15 12:36:00 +08:00
52b26900c5 MUL-8012: fix(inbox): restrict notifications to current workspace recipients (#9242)
* fix(inbox): restrict notifications to current workspace recipients

Removing a member or leaving a workspace deletes their subscriptions, but
several notification sources keep addressing the user by a retained ID:
reactions to their issues and comments, retained assignments, explicit
mentions, squad mentions through surviving squad-member rows, quick-create
outcomes and children_done for a retained parent assignee. CreateInboxItem
inserted those rows unconditionally and the inbox:new event was then sent to
the user's own connections, so a former member could keep receiving new
issue titles and comment content.

Guard the shared insert instead of each source: a member recipient must still
be a member of the item's workspace and an agent recipient must belong to it.
An ineligible recipient yields no row (pgx.ErrNoRows), which every caller now
treats as a quiet skip without publishing inbox:new or failing the underlying
operation. The check runs in the statement's snapshot and takes no row locks;
historical inbox rows are left untouched.

Existing delivery tests created recipients without membership rows; their
fixtures now add the membership the production path requires.

* test(inbox): assert children_done settles receipts when the recipient left

* refactor(inbox): centralize eligible notification writes

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

---------

Co-authored-by: 千乘妍 (Xiaoyaner) <[email protected]>
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-14 14:55:37 +08:00
191c782d60 MUL-7988: recover daemon when profile health port is unavailable (#9217)
* fix(daemon): recover unavailable profile health ports

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

* fix(daemon): wait for profile shutdown before restart

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

---------

Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-13 14:25:49 +08:00
c6b451c9c1 fix(views): route plain attachment download links through the re-sign path (MUL-7991) (#9211)
* fix(views): route plain attachment download links through the re-sign path (WK-11)

A plain markdown link to the API-host attachment endpoint
(MULTICA_PUBLIC_URL/api/attachments/<id>/download, persisted by
buildMarkdownURL in private-storage deployments) opened with a bare
navigation. The browser sent no credential there — web's session cookie is
host-locked to the API origin and SameSite=Strict, and Desktop/mobile carry a
bearer token no <a> can send — so the request returned 401
"missing authorization" and the user downloaded the error body, not the file.

File-card/image and the download button already route through
useDownloadAttachment, which refreshes authenticated metadata and navigates to
the credential-bearing URL the server mints per click (capability / presign /
CloudFront). Plain links missed that path. Intercept attachment download URLs
(UUID-guarded via attachmentIdFromDownloadURL) in RichLink and hand the
click to the same download hook, so all three attachment entry points behave
alike. Modified clicks still open natively; the raw URL remains the durable ref.

Add a routing test covering the UUID-link interception and the non-UUID
non-interception.

Fixes WK-11.

* fix(attachments): scope Markdown downloads to their deployment and workspace

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

---------

Co-authored-by: Dev (multica) <agent@multica>
Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-13 14:23:55 +08:00
53b1baba0a feat(cli): add opt-in daemon autostart (MUL-7731) (#8864)
* fix(test): isolate cmd/multica suite home resolution on Windows

os.UserHomeDir reads USERPROFILE, not HOME, so every test that redirected
only HOME kept resolving ~/.multica against the real home: mkProfiles
stopped isolating at all, and a full-suite run wrote SaveCLIConfig
fixtures over a real default-profile config.json.

Redirect HOME and USERPROFILE process-wide in TestMain, and make mkProfiles
redirect both so per-test isolation holds on Windows too. On unix nothing
changes: USERPROFILE is unused there.

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

* feat(cli): register daemon boot autostart on every platform

multica daemon start now registers the profile's daemon to come back at
login/boot so a reboot no longer takes the runtime offline: an HKCU Run
key on Windows, a launchd LaunchAgent on macOS, and a systemd user unit
(or XDG autostart fallback without systemd) on Linux. New
'daemon autostart enable|disable|status' manages the registration;
--no-autostart opts a single start out, and daemons spawned by a manager
(MULTICA_LAUNCHED_BY, i.e. the Desktop app's own start-at-login pref) are
never registered.

The registered command runs 'daemon start --foreground' per profile and
snapshots the shell's PATH so login sessions keep finding agent CLIs.
EnsureHiddenConsole now hides a console that exists only for the daemon
itself — a Run-key launch would otherwise park a visible console window
for the daemon's whole lifetime.

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

* refactor(cli): make boot autostart registration opt-in per review

Registration now happens only through 'multica daemon autostart enable':
daemon start (and setup / restart / --foreground) merely print the enable
hint when nothing is registered and silently refresh an entry Multica
owns — marked by a comment line in a unit/.desktop file or a ManagedBy
key in a plist (the Run value name doubles as the marker on Windows).
Refresh skips anything without the marker and skips entirely under an
external systemd unit (INVOCATION_ID with a cgroup that is not ours), so
a hand-written unit like the one in #7091 is never overwritten.
--no-autostart is gone (there is nothing to opt out of), loginctl
enable-linger is no longer run automatically (enable prints the command
instead), and 'autostart enable' now spells out that only PATH carries
into the login session while shell-exported variables do not.

Also fixes the binary-update restart handoff under our own systemd unit:
spawning a successor and exiting 0 lets systemd's cgroup cleanup kill it
while Restart=on-failure never fires, leaving the runtime offline after
the first auto-update. The daemon now exits with
daemonSystemdHandoffExitStatus (42) instead; the generated unit carries
RestartForceExitStatus for it, and linux tests cover the unit-ownership
detection behind both the handoff and the refresh gate.

Review items also addressed: gofmt applied to cmd_daemon_test.go and
proc_windows.go, the Desktop wording corrected to its app-start daemon
preference (it has no OS login item), and CLI_AND_DAEMON.md plus the
daemon start / autostart help text follow the opt-in flow.

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

* fix(test): redirect USERPROFILE alongside HOME in cmd/multica tests

Every t.Setenv("HOME", ...) in this package now goes through
redirectTestHome, which sets both home environment variables. Production
resolves ~/.multica via os.UserHomeDir — USERPROFILE on Windows — so a
HOME-only redirect split the fixture's write path from the code's read
path: on Windows the fixture landed where the code never looked (or,
before TestMain's scratch home, in the real ~/.multica). CI on ubuntu is
unaffected; the Windows host suite stops depending on whatever happens
to be in the real profile directory.

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

* fix(cli): record autostart with the stable brew path, shared with the restart target

The exit-42 systemd handoff restarts whatever ExecStart recorded, and
autostartSpecFor recorded os.Executable() verbatim — on brew installs
(Homebrew on Linux too, reached through install.sh) that is the versioned
keg path, which `brew upgrade` deletes when it cleans the old keg:
systemd then fails ExecStart with 203/EXEC and gives up after
StartLimitBurst, leaving the runtime offline until someone starts it by
hand; a LaunchAgent would point at a missing file at the next login.

Fix per review: both consumers now resolve through one shared helper,
cli.StableExecutablePath, with identical detection order — the path's own
known-Cellar shape first, then `brew --prefix` — so the two cannot drift.
The daemon's restartTargetBinary keeps its seam-based per-process cache
but delegates the mapping; autostartSpecFor runs
cli.StableSelfExecutable before recording. cli.IsBrewInstall shares the
same detection core.

Tests pin the keg -> <prefix>/bin/multica mapping at the helper level for
all three known prefixes, and end to end through autostartSpecFor feeding
a brew keg path, as requested in the review.

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

* fix(cli): key autostart ownership on file presence, not enablement

Review round 3: on Linux Enabled only means "linked in
default.target.wants", so the enable/disable guard (Enabled && !Managed)
missed a hand-written unit at our path that was unlinked — or linked
under another target such as graphical-session.target — and disable
deleted it; separately, the XDG sweeper deleted an unmarked
multica-daemon.desktop outright from enable, disable, and the silent
daemon start refresh.

- autostartState gains Present ("a file/value exists at our path");
  refuseUnmanagedAutostart and the status note key off Present, while the
  enabled/disabled verdict still keys off Enabled.
- platformRemoveAutostart re-checks the marker on the unit BEFORE
  systemctl disable (refusing must not unlink the user's unit as a side
  effect) and refuses to delete an unmarked file; removeXdgAutostartFile
  removes only marked files, which covers all three of its call sites
  (enable's stale sweep, the daemon start refresh, disable).
- sync no longer hints enable at a foreign unlinked file, and never
  heals an owned-but-unlinked entry — refresh must not re-enable what the
  user manually disabled — it hints instead.
- linux tests cover unlinked / other-target-linked / default-linked
  unmarked units on the read side, the removal backstop, a managed unit
  coexisting with an unmarked .desktop, and the mark-checked XDG
  sweeper; shared guard/status/sync tests move to the presence contract.

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

* fix(cli): keep the running service's restart policy when disabling autostart

Review round 4 (P2): on Linux, `autostart disable` deleted the managed
unit file after unlinking it. The running daemon kept going, but systemd
lost the unit — LoadState=not-found, Restart=no — so the next
binary-update handoff (exit 42) exited into a service that could no
longer restart it and the runtime stayed offline until a manual start.

Disable now removes the enablement only: unlink every wants entry
(systemctl --user disable, with a glob fallback for a missing user bus)
and KEEP the marked unit file, so the supervised process retains its
restart policy while nothing starts at the next login. Re-enabling
re-links the same file. A second disable is a no-op again (change is
keyed off links actually present, in any target). The macOS equivalent
is fixed in the same spirit: the plist is deleted WITHOUT launchctl
bootout, which used to terminate the daemon launchd was supervising —
disable means "don't start at the next login", not "kill this session".

enable/disable now read the state once and share it with the ownership
guard; enable's wording keys off the pre-state, because re-linking a
kept unit often changes no file content and "already enabled" would
describe the state the user just left. The daemon-start hint now fires
only when nothing is registered at all — never at a disabled-or-foreign
file the user chose or owns — and the refresh still never re-links.

Regression coverage for running service → disable → handoff:
TestDisableKeepsRunningServiceRecoverable asserts the unit file survives
with RestartForceExitStatus intact, no wants link remains, the handoff
still detects our own unit (exit-42 path), a subsequent sync does not
re-enable, and a second disable reports no change. Docs note the
keep-the-file semantics on both Linux and macOS.

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

* test(cli): update the coexistence test to the keep-the-unit disable contract

CI caught the round-4 test asserting the pre-round-5 behavior: it wrote a
marked unit WITHOUT a wants link and expected platformRemoveAutostart to
delete it. Disable now keeps the marked unit (unlinking its enablement)
so a running service retains its restart policy — so the test links the
unit (giving disable real work, changed=true), asserts the unit survives
unlinked, and keeps its original point: the hand-written .desktop beside
it is never touched.

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

---------

Co-authored-by: ZhangRui <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Bohan-J <[email protected]>
2026-10-12 00:17:42 -04:00
jikuya-kunihikoandjikuya d6956ede15 feat(daemon): report full CLI build commit in local identity (MUL-7951) (#9164)
Co-authored-by: jikuya <[email protected]>
2026-10-11 23:35:55 -04:00
c8d6a1fad0 MUL-7923: fix(test): isolate test home on Windows so tests stop overwriting ~/.multica/config.json (#9128)
* fix(test): isolate test home on Windows across config-writing packages

Tests that only redirect HOME still resolve ~/.multica against the real
home on Windows: os.UserHomeDir reads USERPROFILE, not HOME. SaveCLIConfig
then writes test fixtures over a real default-profile config.json.

Reproduced on a Windows host by running `go test ./internal/cli ./cmd/multica`
on an unmodified tree: the real ~/.multica/config.json was replaced with the
test fixture {"token": "mul_owner_pat"}, dropping server_url, app_url and
workspace_id. The running daemon keeps its in-memory config, so the damage
only surfaces on the next restart as an auth failure.

Redirect both home variables process-wide in TestMain for the packages that
resolve ~/.multica (cmd/multica, internal/cli, internal/daemon), and route
per-test home redirects through a redirectTestHome helper that sets HOME and
USERPROFILE together. Per-test t.Setenv overrides still win.

Comparison on the same host, same packages:

  without this change: 6 internal/cli + 10 cmd/multica failures, real config overwritten
  with this change:    1 + 5 failures (all pre-existing Windows cases), config intact

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

* fix(test): fail closed when isolating test homes

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

* test(ghsnapshot): isolate cursor fixture from other packages

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

---------

Co-authored-by: ZhangRui <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Yushen <[email protected]>
2026-10-10 17:03:32 +08:00
苗大 3bacc41f84 MUL-7573 fix(lark): clear the Typing reaction without trusting in-process state (#8656)
* fix(lark): persist and reconcile typing cleanup across replicas

- preserve terminal trigger targets and per-input cleanup ownership
- reconcile late Adds and source deletion without delaying replies
- recover interrupted ledger indexes and cover lifecycle boundaries

Refs: #8655

* fix(lark): restrict typing cleanup to the current application

- match both app operator type and the current App ID
- preserve foreign-app, human and unknown-owner reactions
- cover ownership filtering in fast and durable cleanup paths

Refs: #8655

* fix(lark): bound typing recovery and credential retention

- recover taskless indicators after lost debounce timers
- cap workspace ledger admission and redact terminal credentials
- isolate maintenance budgets and test retention and quota boundaries

Refs: #8655

* docs(lark): document typing cleanup recovery and rollout limits

- explain ownership, retention, quota and observable terminal outcomes
- document migration upgrades and safe application rollback

Refs: #8655

* fix(deps): require patched Go 1.26.9 and x/net 0.60

- Require the patched Go standard library to clear reachable vulnerabilities.
- Upgrade x/net to the matching HTTP/2 security release.
- Align local development prerequisites with the minimum Go version.
2026-10-08 20:20:15 -07:00
2penheimerandCursor 13520a0d13 fix(cli): keep member names off free-form property lists (#9093)
multi_text and multi_url entries were resolved through the member directory, so a stored string that matched a member reference displayed as that person's name.

Co-authored-by: Cursor <[email protected]>
2026-10-09 01:08:07 +08:00
bb14e8763c feat(issues): add multi_text and multi_url custom property types (MUL-7496) (#8559)
* feat(issues): add multi_text and multi_url custom property types

Workspaces could only hold list-shaped values as preset option lists
(multi_select) or member references (multi_actor). There was no way to
keep a list of free-form strings or links on an issue, so teams with
"related docs / spec links / aliases" needs had to either cram them into
one text field (no structure, nothing clickable) or burn one select
option per link.

This adds two property types:

- multi_text: an ordered list of free-form text entries
- multi_url: an ordered list of http(s) URLs

Each entry is validated exactly like a single text / url value
(length caps, http(s)-only for URLs), duplicates are dropped, insertion
order is preserved, and values are capped at 20 entries to respect the
16KB properties-bag budget. Values ride the existing jsonb bag, so the
jsonb_path_ops GIN index, the @> containment filter, and the client
value schema all keep working unchanged.

Filtering matches any single list element exactly (equality) plus "No
value" — the containment form the filter compiler already emits for
array elements, so no new server predicate is needed. List types
deliberately get no sort order and cannot group, like the other
multi-value types.

On the web, list values render as chips; multi_url chips are clickable
and open in a new tab, and the editor popover offers a per-entry open
button, so several related documents or links stay directly reachable
from the issue. The filter menu reuses the scalar input (equality-only
plus "No value"). CLI: comma-separated --value form for both types,
element-equality --property filtering, and display_values rows.

Server-side allowlist widened by migration 500 (check constraint
re-added NOT VALID then validated, same pattern as migration 341).

* fix(properties): address list property review nits

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

* fix(properties): scope list drafts to the edited issue

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

* test(agent): avoid fork races when writing zeroclaw fixtures

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-07 13:33:02 +08:00
Bohan Jiangandmultica-agent 43b0571f99 MUL-7821: feat(cli): mark duplicates with issue status/update --duplicate-of (#8979)
* feat(cli): mark duplicates from issue status and issue update (MUL-7821)

Add --duplicate-of to `multica issue status` and `multica issue update`.
It resolves an issue key or UUID and sends duplicate_of_issue_id, which
the server already accepts (MUL-7349). The CLI rejects a status other than
cancelled, and description or attachment changes, before any request or
upload. It fails when the response has no duplicate_of field, so a server
older than v0.5.2 cannot silently drop the mark.

Closes #8974

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

* docs: teach agents to mark duplicates with --duplicate-of (MUL-7821)

Add the command to the agent runtime brief, the multica-platform issues
reference, CLI_AND_DAEMON.md and the CLI docs tables, so agents record the
mark instead of cancelling a duplicate and explaining it in a comment.

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

---------

Co-authored-by: multica-agent <[email protected]>
2026-09-29 23:27:27 -07:00
fa5d470ae4 MUL-7706 feat(wecom): every notice reads the language of whoever will read it (#8831)
* feat(wecom): every notice reads the language of whoever will read it

The adapter's user-visible strings were Chinese literals scattered across
replier.go, inbox_message.go and outbound_media.go, so an English-reading member
got Chinese notices whatever their profile said. copyPack already existed for
the bubble's five stream strings; this moves the rest into it and resolves the
locale from the destination rather than the deployment.

Who decides, per surface:
  - a 1:1 reply, the binding prompt, the inbox card: the reader's own profile
  - a group notice: the room, which reads the deployment's language
  - an attachment-failure notice: resolved on the request path and carried on
    attachmentTarget, because the failure happens on a detached goroutine with
    no context left to read a profile with

TestOnlyStringsGoHoldsUserVisibleCopy walks the package's AST and fails on any
Han literal outside strings.go. Two files are listed as pending with an exact
count: wecom_channel.go's unsupported-kind receipt and media_ingest.go's two
media notices, each moved by its own follow-up. The count is asserted both ways,
so the allowance cannot outlive the follow-up that consumes it.

* feat(wecom): every notice reads the language of whoever will read it

The adapter's user-visible strings were Chinese literals scattered across
replier.go, inbox_message.go and outbound_media.go, so an English-reading member
got Chinese notices whatever their profile said. copyPack already existed for
the bubble's stream strings; this moves the rest into it and resolves the locale
from the destination.

Who decides, per surface:
  - a 1:1 reply, the binding prompt, the inbox card: the reader's own profile
  - a group notice: the room, which reads the deployment's language
  - an attachment-failure notice: resolved on the request path and carried on
    attachmentTarget, because the failure runs on a detached goroutine with no
    context left to read a profile with

The wiring is the part that has to hold: router.go passes Languages, and
NewOutboundReplier warns when it is nil, because a missing lookup has no other
symptom — nothing errors, nothing is empty, every notice simply comes out in one
language. TestWecomReplierGetsItsLanguageLookupOnTheRealBootPath asserts it off
NewRouter itself, with the anti-vacuity check read as a delta between two
routers rather than a count, since chat:done has listeners of its own.

TestOnlyStringsGoHoldsUserVisibleCopy walks the package AST and fails on any Han
literal outside strings.go, naming the two files whose copy has not moved yet
with an exact count, asserted both ways.

* MUL-7706 docs(wecom): drop the duplicated stream-field docs, refresh the lint header

The restored StreamNoReply block carried its opening paragraph twice, and the
lint file's header still described two rules and a greeting after the second
rule was dropped.

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

* MUL-7706 test(wecom): compare two routers in the replier wiring guard

A bare chat:done count is non-zero whenever Slack or DingTalk is configured, so
the guard passed with the WeCom block never entered. Build one router without
the WeCom key and one with it, as wecom_bubble_wiring_test.go does, and require
the difference.

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

* fix(wecom): a relayed reply's attachment notice reads the reader's language too

deliverRelayed builds its own attachmentTarget and never set Locale, so
copyFor("") fell back to the deployment pack. On a multi-replica deployment
that is the common path, not the exception: chat:done lands wherever the run
finished, and only the lease holder can write to the socket — so an English
reader whose file fails to upload got the Chinese notice, while the same
failure on the direct path was already answered in English.

Nothing else differs when this is wrong. The file still fails, the notice still
goes out, and no existing test covered the relayed case.

Also two wording fixes against the product glossary: /issue creates an issue,
not a task, so IssueUsage now reads like Slack's; and the task_failed inbox
label is 'Run failed', matching what the web inbox shows for the same
notification.

* MUL-7706 test(wecom): keep each attachment-notice test under its own doc comment

The relayed test was inserted between the direct test's doc comment and its
function, so the direct test lost its comment to the relayed one. Put the
direct test back under its comment, follow it with the relayed test, and move
the util import out of the standard-library group.

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

* MUL-7706 test(wecom): the invoke-denied notice reads its one reader's language

A 1:1 refusal joins the per-outcome locale table; a group trigger's refusal,
sent to the sender's own 1:1, must read the sender's profile rather than the
room's deployment default.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-27 23:00:52 +08:00
3d2592a9a2 MUL-7754: feat: local search index for web and desktop (#8891)
* feat(server): add a change log and sync endpoints for local search (MUL-7754)

Web and Desktop will keep a per-workspace copy of issue, comment, and
project text for instant search. Triggers on issue/comment/project record
the xid of each entity's latest write in search_index_change, covering
every write path including hard and cascaded deletes. Clients bootstrap
with GET /api/search-index/manifest + /snapshot pages and catch up with
POST /api/search-index/changes, whose cursor is a pg_snapshot: a change is
returned once it is visible in the target snapshot but not in the one the
client holds, so late commits are never skipped and long transactions do
not stall catch-up.

Workspace teardown skips the triggers and clears the table, and an hourly
scheduler job prunes rows older than 30 days; a cursor older than the
prune mark gets 410 and rebuilds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(core): add a synced local search index for web and desktop (MUL-7754)

Each (user, workspace) gets an IndexedDB copy of its issue, comment, and
project text, kept in a worker (SharedWorker when available, otherwise a
dedicated worker per tab) and searched in memory. Matching, ranking, and
snippets port the server's issue/project search, so local rows have the
server's shape and order.

The worker bootstraps from /api/search-index/manifest + snapshot pages,
catches up through /changes after realtime events, reconnects, focus, and
every five minutes, and resumes an interrupted bootstrap. It has no
credentials of its own: tabs run its requests through their API client.
Local results are served only while the copy is complete and recently
confirmed; otherwise searchIssues/searchProjects fall back to the server.
Workspaces over 500 MB of text, lost access, and expired cursors are
handled by declining, wiping, and rebuilding respectively. Session cleanup
deletes every local index.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): search through the local index first (MUL-7754)

The command palette, @mention suggestions, the issue picker, and duplicate
suggestions now call the core local-first search. The palette drops its
300 ms debounce while the local index is serving, since no request leaves
the tab.

An E2E spec checks that palette results match server ranking without any
server search request, follow issues created and deleted through the API,
and that logging out deletes the local copy. .env.example documents the
FF_LOCAL_SEARCH_INDEX kill switch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(core): close local search gaps found in review (MUL-7754)

- Access loss: deleting, leaving, or being removed from a workspace now
  destroys its local copies right away (worker and tab both delete the
  database, since a full-page relocate may end the worker first). The sync
  hook also prunes every copy outside the user's current workspace list,
  which covers access lost while offline or on another device; copies a
  tab is showing are kept in case the list is stale.
- Memory budget: the engine counts UTF-8 bytes the way the manifest does,
  and the budget is checked on every load, snapshot page, and catch-up
  batch, not only at bootstrap. An over-budget copy is dropped and search
  falls back to the server until the workspace is measured again.
- Frozen tabs: a port the sweep dropped is restored with its workspace the
  next time the tab sends anything, a page restored from the bfcache
  attaches again, and a null local answer clears the tab's "serving" flag
  so the palette goes back to debounced server search.

E2E now also covers being removed from a shared workspace.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-27 22:02:13 +08:00
5ed09342af MUL-7680: issue wakeup v2 — conditions, runaway protection, check-ins and visibility (#8807)
* feat(wakeups): add rule deadlines and expose the child-done system rule (MUL-7680)

Wakeups can now end: an absolute deadline (expires_at) or a relative wait
(expires_in_seconds) that restarts when the rule is re-enabled. When the
deadline passes first, the scheduler ends the rule; event rules with
on_timeout=wake run the target once with a wakeup.timeout fact. A timed-out
rule keeps disabled_at NULL so its timeout run stays claimable.

The implicit "wake the parent's assignee when a stage of sub-issues
finishes" behavior is now described by GET /api/issues/{id}/system-wakeups
and can be turned off or given a supplementary instruction per issue via
PUT /api/issues/{id}/system-wakeups/child_done. Rule reads fail open to the
previous behavior.

List responses add the creator (member or agent) and the new expiry fields.
The CLI gains --expires-in, --expires-at and --on-timeout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(issues): let members create wakeups and show the child-done system rule (MUL-7680)

The issue sidebar's Wakeups section is always available on open issues and
gains a New wakeup popover. Members pick a condition (a time, a recurring
check with an end date, someone's reply, an agent's run ending, or raw
events), the agent to wake, the instruction, how often it fires, how long
to wait and what happens on timeout. It posts the same configuration agents
create with the CLI.

The parent's child-done wake appears as a System rule with its stage,
remaining sub-issues and target, a per-issue toggle and a supplementary
instruction. Rows show when a rule ends, timed-out rules read as such, and
details name who created the rule. Rule titles wrap to two lines instead of
truncating.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* docs(wakeups): document deadlines, member-created rules and the system rule (MUL-7680)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(wakeups): platform conditions, runaway protection and check-ins (MUL-7680)

Conditions: a wakeup can now wait for a fact the platform checks itself
(a status, assignee, label or property value; sub-issues or one stage
finishing; a linked PR's checks finishing or merging; another issue's
status). Related events only prompt an early evaluation; the rule wakes
its agent when the predicate becomes true, and a repeating rule fires
again only after it turned false or its facts changed. Finished PR checks
from before registration are ignored.

Runaway protection: repeating event rules stop after max_fires runs
(default 20), a rule pauses when its causal chain passes through it a
third time without a person in between, or when it starts 12 runs in an
hour. The reason is stored and shown.

Also: silent check-ins for every/cron checks (the run then posts no
fallback comment), wake now, delete, a per-rule run history, paused-rule
lists, timeline entries for created/triggered/timed-out/paused/check-in,
and source, paused scope, 7-day runs and child-done system rows in the
workspace list. The CLI gains --until-* conditions, --max-fires and the
trigger/delete/checkin/runs commands; the brief and wakeup prompt state
the check-in exception.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(issues): record child-done transitions in the status write (MUL-7680)

The child-done system rule ran after the status write committed and gave
up on any error, so a crash, deploy or transient failure lost the
parent's wake. A trigger now records each child's move into a closed
status in the writing transaction, for every writer. The request that
wrote it processes the rows right away and a scheduler sweep retries
anything left over; claims make the two paths process a transition
once.

The rule also follows a workspace default (settings key
system_wakeup_child_done) until an issue sets its own, the per-issue
update accepts partial bodies, and each trigger adds a timeline entry
that names its system comment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): wakeup conditions, history, timeline and workspace list (MUL-7680)

Issue sidebar: members can create the platform-checked conditions (a field
reaching a value, sub-issues or a stage finishing, a linked PR's CI or merge,
another issue's status) and cap how often a repeating wait fires. Rule
details show the fire count, why the platform paused a rule, the latest runs
with silent-check notes, and offer wake now and delete.

The issue header says what the issue is waiting for ("Emacs 在等 Jiayuan 回复
+2") and opens the Wakeups section. Board and list cards use the same short
sentence, or flag a paused rule. Wakeup runs read as "由唤醒触发 · <condition>"
in the execution log, transcript and on the comments they post.

The timeline shows created, triggered, timed-out, paused and check-in entries
with the rule they came from; consecutive check-ins merge, and the child-done
entry stands in for its system comment, which it can reveal.

Automation → Issue wakeups adds a source column and filter, 7-day runs, a
paused scope with a banner, the child-done system rule on each waiting parent,
and "New wakeup" with an issue picker. Settings → Issue statuses sets the
system rule's workspace default. Copy in all five languages.

The server records the watched issue's identifier in other-issue conditions
and the rule's creator in timeline snapshots, and returns board summaries with
their condition.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* docs(wakeups): document conditions, runaway protection and check-ins (MUL-7680)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): list system rules without a revision and localize statuses (MUL-7680)

System rule rows returned revision 0, which clients reject as a rule
revision; they now return null, and a list row with an invalid revision no
longer fails the whole page. Built-in statuses in conditions and the status
picker read in the viewer's language. Adds a browser test for a condition
created from the form, the header summary, the scheduler waking the agent
and the workspace list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(views): say why a rule paused without repeating "paused" (MUL-7680)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(i18n): use the product's task terms in wakeup copy (MUL-7680)

ja, ko and fr wakeup strings said イシュー / 이슈 / ticket; the product term
for an issue in those languages is タスク / 태스크 / tâche (and sub-tasks
accordingly), per the conventions page. French strings are rewritten for
the feminine noun.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* test(views): pin the shortcut platform in the wakeup form test (MUL-7680)

The send shortcut is primary+Enter, which is Ctrl on Linux CI runners, so
pressing Cmd+Enter only submitted on macOS.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(views): simplify the workspace wakeup list

Put the filters on one row: scope as a segmented control on the left,
source/trigger/agent and a search-as-you-type box on the right. "New
wakeup" moves to the page header, which drops the separate Search button.

Rows are single-line: the identifier sits before the title, frequency and
timezone move into the trigger tooltip, and selection, prompt edit and
the last run's transcript show on row hover or focus. The issue column
takes the remaining width and truncates, so the table fits the page.

Also fix creating from the list: the issue picker closes itself right
after reporting the selection, and that close cancelled the flow before
the form could open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* refactor(server): run the child_done rule as a system issue wakeup

The parent-assignee wake on closed sub-issue stages was its own code path:
a system comment, a mention-style run, an override table and endpoints.
It is now an ordinary issue_wakeup row with system_rule set, sharing the
condition, receipts, runaway protection, timeline entries and run model of
people's rules.

- One children_done evaluator: a stage wakes the assignee when it closes
  while a later stage waits; the wrap-up waits for every sub-issue,
  unstaged ones included. People's sub-issue conditions read the same set.
- issue_child_event records closing, reopening, re-parenting and restaging
  for every writer; the request processes it right after commit and the
  sweep retries. User sub-issue rules become immediate too.
- The target is resolved when it fires: an agent run, a squad leader run,
  an inbox notification for a member, or only the timeline entry.
- A parked (backlog) parent catches up when it leaves backlog; a waiting
  run of the same agent is joined instead of queuing a second one; the
  hourly limit pauses a runaway rule. No system comment is posted.
- The run gets the issue's instruction, else the workspace's, else the
  built-in one, plus each stage's counts and the next stage.
- Workspace defaults move to GET/PUT /api/system-wakeups (owners and
  admins) and apply to every rule nobody customized; turning a rule on
  records what already holds so old facts do not fire.
- Existing open parents get their rule from a one-time backfill.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): workspace wakeup settings and sub-issue notifications

- Settings → Wakeups holds the sub-issue rule's workspace default and
  default instruction; the toggle leaves Issue statuses.
- The issue's system rule shows who it reaches (a member is notified),
  a platform pause, and the instruction it will use.
- Timeline entries say whether the assignee was woken, notified or joined
  a waiting run. The folding of the old system comment is gone.
- Inbox renders the new children_done notification on web, desktop and
  mobile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(server): stop wakeups from repeating their agent's runs

Runs are serialized per issue and agent, so a wakeup never ran beside
another run of its agent, but it queued behind one and repeated it. Each
rule now checks two things before it starts a run:

- Acknowledged: every input came from the agent itself. Its own comments
  and issue changes never wake it, and a condition its own unfinished run
  on the issue satisfied does not wake it when the platform or the agent
  set the rule up. This fixes #8849: a coordinator closing its own stage is
  not woken again while that run is going. A person's condition rule still
  runs afterwards, since the running agent lacks its instruction.
- Merged: when a run of the agent is already waiting to start on the issue
  (assignment, comment, another rule), the rule's instruction and facts
  join that run (context.wakeup_joined, sent to the daemon as
  wakeup_joined and rendered for every prompt kind) instead of queuing
  another. Turning the rule off or replacing it withdraws what it joined.

Sub-issue changes now record the run that made them, and hints and
condition.met inputs carry those runs so a satisfied condition knows
whether the agent caused it. A rule that names the agent itself as the
actor keeps waking it on its own changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): show merged and acknowledged wakeups, hint assignee comment rules

- Timeline entries say when a wakeup joined the agent's waiting run or was
  the agent's own doing, for people's rules and the sub-issue rule.
- The create form tells a member that a reply or comment rule for the
  issue's agent assignee joins the run a comment already starts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(server): hand waiting wakeups to a run when it is claimed

A rule that fired while its agent had a run waiting used to write its
instruction into that run right away and consume its inputs. That let a
member's rule run under another member's identity, kept deleted rules'
instructions in the run, lost the inputs when the run was cancelled or
claimed by an older daemon, and skipped the fire cap and loop check.

Now the rule only waits for a run that runs as the same person its own
run would, keeping its inputs. When a daemon advertising
joined-wakeups-v1 claims that run, JoinWaitingWakeups rechecks each rule
(on, same agent and person, creator still allowed, not the agent's own
input, no loop), consumes its inputs, counts the fire toward max_fires,
adds its chain and records the merge. A re-claim drops entries of rules
turned off or changed since. Anything else leaves the rule its inputs to
start its own run.

A child_done rule created by the backfill or by processing a change now
treats sub-issues with unprocessed close events as still open, so the
backfill no longer swallows a close waiting for the sweep.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(server): consume joined wakeup inputs only once the run starts

A claim used to consume the inputs of the rules that joined the run. If
that claim did not go through, the run went back to the queue carrying
inputs the rules no longer had: turning the sub-issue rule off on the
issue or for the workspace (which only changes enabled) did not remove
them from the next claim, and cancelling the run lost a once rule that
had already ended.

A claim now reserves the inputs for the run (receipt task_id, still
unprocessed). The rule's next dispatch settles them: a run that started
consumes them as a merged firing (once ends, max_fires counts and can
pause, timeline entry); a run that ended without starting gives them
back; one that has not started keeps them. Turning a rule off or
changing it discards its pending inputs, reserved ones included, and a
later claim of the same run rechecks every entry and drops the ones
without reserved inputs or no longer allowed to reach the run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-27 21:08:54 +08:00
815fe37b1f MUL-7685: fix(agents): paginate task history and aggregate duration (#8804)
* fix(agents): paginate task history and aggregate duration

* test(agents): cover pagination boundaries and query lifecycle

* fix(migrations): register agent history index retry cleanup

* fix(agents): show totals only for complete task history

* fix(agents): average duration over the full rolling window

The server bounds duration totals by completed_at > now() - 30 days,
which can span 31 calendar days. deriveAgentActivity summed duration only
after trimming buckets to the chart's 30 local-day slots, so runs on the
oldest partial day were dropped from the average. Sum duration for every
returned bucket before slotting.

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

* fix(migrations): renumber agent history index to 552

main now carries 551_pr_merge_status, and migration numbers from 129
onward must be unique (TestMigrationNumericPrefixesAreUnique).

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-27 15:58:00 +08:00
importcpp 04cdd4857d fix(notifications): ignore malformed member mention IDs (#8876) 2026-09-27 13:32:11 +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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-27 03:23:21 +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
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 <[email protected]>
2026-09-25 14:36:02 +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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
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
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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: zhudejun1 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Multica Agent <[email protected]>
2026-09-24 13:07:25 +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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-24 02:58:19 +08:00
d45aba1cd7 fix(tasks): settle supplements in application transactions (MUL-7630) (#8755)
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-23 17:41:27 +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 <[email protected]>
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 <[email protected]>

* fix: address MUL-7565 review feedback

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

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

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

* refactor: remove MUL-7565 supplement ordinals

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

* 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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: yushen <[email protected]>
2026-09-23 16:10:07 +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 <[email protected]>
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 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-23 15:18:27 +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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-23 14:59:47 +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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
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 <[email protected]>

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

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

* test(issues): isolate commit failure task assertions

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

---------

Co-authored-by: Forge-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
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
8f1ad46ee8 MUL-7556: index GitHub PR address lookups (#8636)
* perf(db): index GitHub PR address lookups (MUL-7556)

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

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

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

* fix(migrations): renumber GitHub PR address index

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

---------

Co-authored-by: Forge-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-22 15:33:23 +08:00
Multica Eveandmultica-agent 8c9f865e3e MUL-7480: Revert comment steering feature (#8648)
* Revert "MUL-7480: Link delivered steers to final replies (#8623)"

This reverts commit 3de6275eb3.

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

* Revert "fix: preserve active task intent in steer frames (#8621)"

This reverts commit b866dacd1c.

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

* Revert "MUL-7480: Steer active agent turns from comments (#8605)"

This reverts commit 8ce795b007.

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

* chore(db): remove reverted comment steer schema

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

---------

Co-authored-by: Sol-Boy <[email protected]>
2026-09-21 18:29:50 +08:00
VvVandweiweiwei 2b62b2b52e fix(cli): compare workdir paths in one namespace (MUL-4252, MUL-7193) (#8028)
* fix(cli): compare workdir paths in one namespace (MUL-4252)

The containment guard behind --content-file, --description-file and --attachment
canonicalizes the workdir but could not canonicalize a candidate path that does
not exist yet, and compared the two sides in different namespaces. That is wrong
in three ways, and only the first is merely a wrong answer.

## A path inside the workdir read as outside

filepath.EvalSymlinks fails outright when any component is missing. The fallback
resolved one level up (filepath.Dir) and otherwise called filepath.Clean, which
does not follow symlinks at all.

os.Getwd() returns the LOGICAL working directory whenever $PWD names the current
directory, and everything that writes $PWD carries the path as typed: a shell's
`cd`, and the PWD=<workdir> this repo exports to agent processes
(pkg/agent/opencode.go, deveco.go, codearts.go). So on a host whose workdir path
crosses a symlink, the base was /private/var/... while the candidate stayed
/var/..., and `--content-file reports/summary.md` was rejected as a guardrail
violation when the real problem was that `reports/` did not exist. The only
remedy the error offers is --allow-external-file, so the caller's next move is to
disable the guard and collect an unrelated not-found error on the retry.

## A path outside the workdir read as inside

With an already-canonical workdir the same fallback inverts. `escape/sub/x.md`,
where `escape` is a symlink pointing out of the workdir, read as INSIDE it: the
unresolvable tail fell back to a lexical clean that never sees the symlink.

## ".." was applied to the string, not to the followed path

filepath.Join and filepath.Clean collapse "escape/.." before any symlink is
followed, while the kernel follows `escape` out of the workdir first and then
goes up. So the guard judged a different path than os.ReadFile then opened:

  guard says within=true ; os.ReadFile got "another run's file"

That is the MUL-4252 failure this guard exists to prevent — reading a file left
behind by another run — reachable with a file that exists and no race.

## A path on another volume read as an internal failure

Windows only: filepath.Rel cannot relate two absolute paths on different volumes
and returns an error, which surfaced as `Rel: can't make Z:\report.md relative to
C:\Users\...\workdir`. Two absolute paths Rel cannot relate are on different
volumes, which is as far outside the workdir as a path gets, so the guard now
answers that instead.

## The fix

util.ResolveSymlinksBestEffort canonicalizes as far as the filesystem allows: it
resolves the path as given first — the only step that can see ".." the way the
kernel does — then walks up to the deepest existing ancestor, resolves that, and
re-attaches the missing tail lexically. A mismatch inside the missing tail is
inert: a path whose intermediate directories do not exist cannot be opened at
all. Both sides of the comparison go through it.

internal/daemon/execenv already had this algorithm as resolvePathBestEffort,
unexported and documented as Python's Path.resolve(strict=False). It now
delegates, so there is one owner, and the ".." handling makes it more faithful to
that documented contract rather than less.

## Coverage

TestFileWithinWorkingDir runs its table under two working-directory shapes, and
both are load-bearing: under a symlinked workdir an unresolved candidate always
looks outside, so the symlink-escape rows can only fail under a canonical
workdir. Reverting one part of the mechanism at a time:

  candidate unresolved (Clean)        5 tests / 15 subtests fail
  one-level fallback                  3 tests /  8 subtests fail
  base side left unresolved           5 tests /  9 subtests fail
  clean before resolve ("..")         2 tests /  5 subtests fail
  helper walks only one level          4 tests /  9 subtests fail

The lint side is covered too: classifyLocalPathTarget uses the same predicate,
and there the same false negative flips direction — a workdir-local path with a
missing intermediate directory matched neither of its signals, so the link was
published instead of blocked.

cmd/multica has no Windows job, so the guard's Windows-only behaviour ran
nowhere: a candidate on another volume, and a directory junction, which
filepath.EvalSymlinks resolves to its own name rather than descending through it
(internal/daemon/config_windows_test.go asserts that refusal directly). A
junction inside the workdir pointing out of it therefore still reads as inside,
before this change and after it. Failing closed on a component that exists but
cannot be canonicalized would reject every path under a pnpm-style node_modules
junction, and on a host whose workdir is itself reached through a junction it
would reject every --content-file and --attachment on that host — a larger break
than the one it prevents. The windows-tagged test pins the gap with the
observations it rests on and fails with instructions if the platform starts
resolving junctions. Its CI step is scoped and runs with -v so a silent skip is
visible rather than passing as "ok".

* fix(cli): lead with the missing file when a rejected path does not exist (MUL-4252)

The workdir containment guard answers one question and reports it as if it
answered two. A path outside the workdir that does not exist got the same error as
a stale file sitting in /tmp:

  --content-file path "/tmp/desc.md" resolves outside the current working
  directory; write agent temp files inside the task workdir ... Pass
  --allow-external-file to override.

The only remedy that error offers is the flag, so the caller's reasonable next
move is to disable the guard and retry — earning a second, unrelated "no such
file or directory". The diagnosis points away from the actual problem, and the
lesson it teaches an agent is to turn the guard off.

MUL-4252 was about reading a file that exists and belongs to another run. When
the path does not exist there is no such file to protect anyone from, so the
missing file leads, with the containment problem kept as the second clause since
the caller still needs to know the flag would not help.

pathExists treats any stat error other than "not exists" as "exists", so a
permission wall or a broken mount keeps the guard's own wording and stays
fail-closed rather than being reported as a missing file.

Both halves are pinned by mutation: dropping the branch fails
TestExternalFileErrorLeadsWithMissingFile on the missing-file assertion, and
making pathExists report everything as missing fails the same test on the
assertion that an existing external file must still be told about
--allow-external-file.

* fix(cli): preserve Windows path kinds and resolve junctions in the containment guard (MUL-4252)

Addresses the two blockers in the review on 3ec1781e:

1. absNoClean prefixed the working directory onto every non-IsAbs path,
   but on Windows a root-relative path (\tmp\desc.md) resolves against
   the volume ROOT and a drive-relative path (C:tmp\desc.md) against the
   current directory on that drive. The guard inspected
   C:\task\workdir\tmp\desc.md while os.ReadFile opened C:\tmp\desc.md,
   so an in-workdir shadow could pass an outside read. absNoClean now
   splits per platform (path_other.go / path_windows.go) and preserves
   the kinds; a drive-relative path on an unobservable drive is handed
   back uncanonicalized and fails closed through the Rel branch.

2. The junction case in workdir_guard_windows_test.go pinned a known
   contract violation (EvalSymlinks refuses to descend through junctions
   under the winsymlink semantics this module ships with). Resolution is
   now component-wise on Windows (evalPath) and follows junctions via
   os.Readlink — which answers under both winsymlink settings — with a
   bounded follow budget; reparse points Readlink cannot speak for
   (OneDrive placeholders) stay opaque. A junction out of the workdir is
   rejected, one into it (the pnpm shape) is still admitted, and the
   known-gap test is replaced by cases asserting both directions plus a
   chain and a dead target.

The shared walk in ResolveSymlinksBestEffort is now component-wise on
both platforms: it applies ".." to the resolved prefix even when the
tail is missing (previously the second phase started from filepath.Abs,
which cleans first), and it never depends on what EvalSymlinks does at
a filesystem root. Unix behaviour is unchanged for every shape the
existing tests cover; the new dot-dot-in-the-walk case pins the
difference.

* fix(cli): classify link targets with the full Windows path grammar and resolve unknown reparse points by handle (MUL-4252)

Addresses the two blockers in the review on d3d370377:

1. Link targets were spliced onto the link's parent unless
   filepath.IsAbs said otherwise, but the kernel applies the full path
   grammar to targets: a root-relative target (\outside) resolves from
   the root of the volume the link sits on, and a volume-GUID target
   (\\?\Volume{GUID}\...) is absolute in the device namespace. The
   resolver now classifies targets through classifyTarget, which reuses
   the same grammar as absNoClean; device-namespace targets without a
   Win32 spelling are kept verbatim so the containment comparison fails
   closed across the namespace instead of splicing a mangled relative
   path (the prefix rules mirror internal/daemon's #6883 handling).

2. An os.Readlink failure on a ModeIrregular reparse point no longer
   means "does not redirect". Readlink decodes only the symlink and
   mount-point tags; other name-surrogates (e.g. container-isolation
   links) redirect through a filter while the lexical location stays
   behind. Such components are now resolved through
   GetFinalPathNameByHandle (zero desired access, so no hydration, and
   FILE_FLAG_BACKUP_SEMANTICS for directories); if the handle names the
   same path the component is opaque, otherwise the walk continues from
   the final path. When even the handle cannot name the location the
   resolver returns the new util.ErrUnresolvablePath instead of
   guessing: fileWithinWorkingDir treats it as outside (fail closed)
   and execenv's delegate falls back to the input, which its
   containment comparison rejects.

ResolveSymlinksBestEffort now returns an error so the fail-closed case
is explicit rather than a look-alike path. Negative tests cover a
root-relative symlink target with an in-workdir shadow (the kernel's
target is verified by content before asserting rejection) and a
volume-GUID junction target with the prefix-stripped shadow.

* fix(cli): classify drive-absolute link targets before the volume check (MUL-4252)

classifyTarget asked VolumeName before IsAbs, but VolumeName is
non-empty for a drive-absolute path (C:\dir) too. A junction's absolute
target was therefore misread as drive-relative and resolved against the
working directory — a nonexistent spelling — which errored the
resolution and left the escape to be judged lexically inside the
workdir. The IsAbs branch is asked first, and the load-bearing ordering
is pinned by TestClassifyTarget, a fixture-free table over the target
grammar that would have caught this without a filesystem.

* ci: run the util link-target classification table on Windows

TestClassifyTarget pins the branch ordering classifyTarget's link-target
grammar depends on, but windows-tagged tests in internal/util had no
execution point: the windows-execenv job only ran the guard tests in
cmd/multica. Scoped to the table by name, matching the job's pattern.

* test(cli): correct classifyTarget expectations for root and base shapes

The walk base is the volume root and the working directory's components
live in the segments for a drive-relative target, and the plain-relative
branch returns the link's directory verbatim without a trailing
separator. The expectations said otherwise; the code was right.

---------

Co-authored-by: weiweiwei <[email protected]>
2026-09-21 16:25:13 +08:00