Commit Graph
2275 Commits
Author SHA1 Message Date
Jagan Fulare a8e68a1b09 feat(admin): expose market catalog 2026-10-31 10:12:06 +00:00
Jagan Fulare 581260b61b test(admin): cover workspace creation 2026-10-31 10:07:33 +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
Jagan Fulare 38497cc321 feat(market): support workflow templates 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 0bef12a93c fix(market): replay install keys before the catalog
Resolve a workspace idempotency key before catalog and admission so a
replay does not depend on the catalog. Adopt a skill only when its
content digest matches and it has no files; any other name hit is
content_mismatch and is not inserted. Keep crash bookkeeping off the
public step while still storing it with the operation.

Co-authored-by: klinsmaya <[email protected]>
2026-10-18 09:48:13 +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 27618e6b67 feat(market): record install steps before the native insert
The ledger commits a correlation id while a step is armed, then inserts
with that id. An ambiguous write stays unknown and that pass only looks
the id up. A ready install keeps a refused task step and does not insert
a task.

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 46fbc25c28 feat(market): add the workspace client and dark flag
Add Market types, zod schemas, client methods, and query options keyed
by workspace. A ready preflight parses only when every check is
satisfied and the source is live; anything else falls back to
unreadable. Generate the block-reason list from the server JSON with a
CI drift check, add the market_v1 flag key, reserve the market slug,
and bucket market routes in diagnostics.

Co-authored-by: klinsmaya <[email protected]>
2026-10-18 03:48:40 +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
Cursor Agentandklinsmaya cd2847b8c3 feat(market): evaluate install preflight without writing
Add evaluatePreflight, a pure check of whether a member could install one
listing version here. It judges the model runtime, MCP tools, Composio
connections, and skills with the predicates the existing routes use, so
preflight cannot promise what those routes refuse. Facts load through a
marketReader of Get and List methods inside a READ ONLY transaction. A
stub listing is never ready.

Share the rules instead of copying them. ListAgentRuntimes reads
runtimesVisibleTo, BuildTaskOverlay reads composio.MountableToolkits, and
canManageAgent decides with canManageAgentEnv, so a NULL agent owner no
longer matches anyone.

Block reasons live in market_block_reasons.json, and a test pins the Go
consts to it.

Co-authored-by: klinsmaya <[email protected]>
2026-10-18 03:34:36 +00:00
Cursor Agentandklinsmaya 2e6b76ee91 feat(market): parse the catalog contract
Add the marketcatalog client. It reads listings and pinned versions from the
NocoBase /market/v1 resources, or serves a stub sample catalog when no catalog
URL is set. One decoder parses both, so a sample cannot hold anything the live
parser rejects. A malformed manifest is ErrInvalidListing and never becomes a
listing. Unknown requirement kinds are kept as unsupported needs.

Co-authored-by: klinsmaya <[email protected]>
2026-10-18 03:23:15 +00:00
700d25eef5 MUL-7420: render the workflow brief once on the server, for the assignee or an observer (#9293)
- issueworkflow.Brief is the only renderer: the claim carries it into every
  run's per-turn prompt; the editor preview and timeline show the same text.
- Workflow handoffs stop writing the brief into handoff_note, so a person's
  note no longer drops the brief and the handoff framing fits again.
- Agents that are not the issue's assignee get an observer brief without
  the step's commands.
- Clearer rule override and narrower wakeup guidance; user text is
  stripped of control characters.
- multica-platform skill documents project workflows.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-16 17:28:45 +08:00
ec1e931742 MUL-8049: fix(workflows): validate the handler in the brief preview (#9294)
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-16 17:25:29 +08: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
9f82479231 MUL-8018: feat(runtimes): price Claude Sonnet 5.5 and Haiku 5.5 (#9253)
* feat(runtimes): price Claude Sonnet 5.5 and Haiku 5.5

* fix(runtimes): align Claude 5.5 metrics pricing (MUL-8018)

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

* test(wecom): make inbox fallback timing deterministic (MUL-8018)

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

---------

Co-authored-by: mika <[email protected]>
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: yushen <[email protected]>
2026-10-16 15:01:27 +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
8352787263 MUL-7658: fix(daemon): recover undelivered batch claims after timeout (ZIC-260) (#8774)
* fix(daemon): recover undelivered batch claims after request timeout

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

* test(daemon): avoid copying task service locks in recovery fixture

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

* test(tasks): isolate delegated recovery assertions (MUL-7658)

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

* test(daemon): use one clock for deferred claim hints (MUL-7658)

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

* fix(daemon): keep claim recovery within client deadlines (MUL-7658)

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

* fix(execenv): preserve racy Git index timestamps in snapshots (MUL-7658)

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

* fix(daemon): deliver claim responses before recovery side effects (MUL-7658)

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

* fix(daemon): isolate HTTP claim retries from recovery (MUL-7658)

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

---------

Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Sol-Boy <[email protected]>
2026-10-15 16:43:24 +08:00
640788c059 MUL-6945 & MUL-6944: fix(daemon): preserve Git content during artifact GC (#7893)
* fix(daemon): preserve tracked files during artifact GC

* fix(daemon): close artifact GC Git preservation gaps

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

---------

Co-authored-by: SSK <[email protected]>
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-15 14:07:09 +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
6d5cdcbed2 MUL-7488: fix(execenv): exclude committed local HEAD changes from resume replay (#8549)
* fix(execenv): exclude clean HEAD advances from resume replay

* fix(execenv): normalize carried WIP before replaying local edits

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

* fix(execenv): resume after conflicting local draft commits

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

* fix(execenv): limit snapshot conflict handling to affected paths

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-14 14:43:47 +08:00
sol713 691e5b48c0 test(wakeup): cover cross-issue conditions through scheduler (#9179) 2026-10-13 17:08:22 +08:00
5436c0c5b6 fix(scheduler): stop a dense-cron autopilot trigger going permanently silent (MUL-7973) (#9191)
* fix(scheduler): stop a dense-cron autopilot trigger going permanently silent

Root cause (#8939): autopilotPlansForScope enumerated (lastPlan, dbNow] under
a 24h replay cap. NextOccurrencesUTC keeps the FIRST 1024 activations of the
interval, so after a pause longer than 1024 occurrences of a dense cron
("* * * * *" after ~17h) the latest kept one is hours stale, fails the
5-minute lateness guard, and the hook returns nothing. Nothing is written to
sys_cron_executions, the anchor never moves, and next_run_at keeps looking
valid while the trigger stays silent for good. Any cron with more than 1024
activations per 24h is affected; */2 (720/day) is not.

Floor `after` at now - maxAutopilotScheduleLateness (inclusive) before
enumerating. This keeps the latest-only collapse, the lateness guard, the
retry path and the cold-start anchors, and makes the 1024 cap unreachable for
this caller; the 24h replayWindow is now redundant and removed.

This also recovers triggers an older build already left silent: the next tick
re-anchors inside the lateness window and the trigger fires again, so the
manual recovery from the report (change the cron, or delete and recreate the
trigger) is no longer needed. An anchor past dbNow (DB clock rollback) is
healed through the same floor instead of keeping (after, dbNow] empty forever.

Tests (jobs_autopilot_long_pause_test.go): dense-cron pauses of 18h and 3 days
fail before and pass after; the short-pause and */2 cases are controls; the
5-minute lateness bound is pinned on both sides; the automatic self-heal and
the future-anchor recovery are covered.
go test ./internal/scheduler/ and go vet ./internal/scheduler/ pass.

Signed-off-by: Battleplus <[email protected]>

* fix(scheduler): keep dense-cron recovery scoped to stale anchors

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

---------

Signed-off-by: Battleplus <[email protected]>
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-13 16:25:22 +08:00
sol713 0f3993e417 fix(cli): classify interrupted upload responses as network errors (#9181) 2026-10-13 16:10:23 +08:00
55c40d5db9 MUL-7982: resume Codex across continued local worktrees (#9218)
* fix(daemon): resume Codex across continued local worktrees

MUL-7982

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

* fix(daemon): serialize Codex session store writers

(cherry picked from commit 7cbe282e1ef5fa8d44e1cfd04def683ad259a360)
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: mameikagou <[email protected]>
2026-10-13 15:05:14 +08:00
Frezc 0a979b1f9c fix(search): classify client cancellation separately from server errors (MUL-7989) (#9209)
* fix(search): classify client cancellation separately from server errors

* ci: rerun checks for search cancellation fix
2026-10-13 14:38:36 +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
Multica Eveandmultica-agent 9310690e0a MUL-7933: fix(wakeups): drain inputs and due checks in one pass (#9202)
* fix(wakeups): drain pending inputs and due checks in one dispatch pass

A dispatch pass visited a single batch of 100 rules ordered by updated_at.
With ~1,160 ready rules in production, a status-change hint waited about
a dozen 30s passes behind condition checks with nothing new, so
--until-status handoffs fired minutes late (MUL-7933, #9139).

Each pass now takes batches from two lanes until both drain or a limit
is hit: rules with pending inputs (at most 50 per batch, not filtered by
enabled) and due schedules/deadlines/condition checks (the rest of each
batch). Rules are visited at most once per pass; the pass stops after 15s
or 1,500 rules so the sequential scheduler keeps time for other jobs.
Global passes log lane counts, duration, stop reason and the oldest
pending input age (warn when cut short or older than 2 minutes).

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

* fix(wakeups): keep due checks moving behind slow inputs; report held inputs apart

Review follow-up on #9202.

- A pass ran a whole batch's inputs before its due rules and checked only
  the overall 15s budget, so a few inputs near the 2s dispatch cap could
  starve schedules, deadlines and condition checks. Dispatches now
  alternate between the lanes, and while due work remains the input lane
  stops at half the pass budget; either lane uses the rest once the other
  drains.
- The pass log counted every unprocessed receipt as backlog, but dispatch
  keeps inputs pending on purpose while a run holds them (reserved by a
  claim, the rule's own run claimed, or waiting to join a queued run). Those
  are now logged as held_inputs, and only the oldest input the scheduler
  could act on (oldest_ready_input_age_s) can raise the 2-minute warning.

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

* fix(wakeups): count only inputs a run can still take as held

Review follow-up on #9202. The pass log's held count hid real backlog in
two cases, so a stalled wakeup could sit for minutes without a warning:

- An input whose task_id pointed at a run that ended (cancelled/failed)
  before starting was counted as held, but takenReceipts releases it on
  the next dispatch. Reserved now means the run has neither started nor
  ended, as takenReceipts decides; an input of a run that started is
  consumed on the next dispatch and also counts as backlog.
- A platform rule's input was held by any queued run on the issue. It is
  now held only by a queued run of the issue's current target agent, run
  as the person the platform rule's run would run as, reusing
  resolveWakeTarget, childDoneRunAs and hasWaitingRun as dispatchSystem
  does.

PendingWakeupInputs is replaced by ListPendingWakeupInputGroups (per-rule
groups, oldest first, bounded) plus pendingInputs in Go.

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

* fix(wakeups): keep tick tests and passes stable under load

CI failed the tick tests with "timeout: context deadline exceeded": under
load the 100ms scan-order touch after each dispatch timed out, and the
pass reported that as an error. The touch only affects scan order (the
rule's outcome is already committed and an unfinished rule stays a
candidate), so a failed touch is now logged at debug, not returned.

The tests also depended on wall-clock speed. The drain tests now run with
a generous budget, since they pin which rules a pass reaches; the slow
input test checks dispatch order (at most two slow inputs before the last
due check) instead of what fits in 1.5s. Without the input budget it
still fails, with all ten inputs ahead of the last due check.

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

* fix(wakeups): scan every pending input group; warn when the scan is incomplete

Review follow-up on #9202. The pending scan read only the oldest 1000
groups, so a ready input behind a page of held groups left
oldest_ready_input_age_s empty, and a scan that hit its 1s timeout was
dropped without a trace; either way a stalled wakeup did not warn.

- ListPendingWakeupInputGroups is keyset-paged on (oldest, wakeup_id,
  reserved); pendingInputs reads pages until a short one.
- The scan gets 3s. On an error or timeout the pass logs
  pending_scan=incomplete with the error, at warn level, together with
  the partial counts; a finished scan logs pending_scan=complete.
- A pass with dispatch failures also logs at warn level.
- Platform-rule target checks are cached per issue.

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

---------

Co-authored-by: multica-agent <[email protected]>
2026-10-13 14:15:10 +08:00
74ff8aabd7 MUL-7983: fix(agent): handle CodeBuddy startup errors without aborting active runs (#9214)
* fix(agent): fail codebuddy runs that emit a top-level error event

CodeBuddy's startup-failure shape is one top-level {"type":"error"}
line followed by a process that stays alive and silent: with an
inherited SERVER__PORT (a user-scope config shared by several tools),
a second concurrent run of the same agent fails to listen, prints
{"type":"error","error":"listen EADDRINUSE: ..."} and never emits
anything again. The parser had no branch for that event type, so the
run sat at "waiting for agent response" until the idle watchdog
stopped it - 19 of 48 runs hung this way on the reporter's machine.

- type=error is now fatal when no terminal result has been seen: the
  CLI's own message becomes the run error and the hung process is torn
  down immediately instead of idling. A result already seen keeps
  governing the outcome, so a late informational error line cannot
  flip a finished run.
- buildCodebuddyEnv now also forces SERVER__PORT=0 (OS-assigned port
  per run, so concurrent runs never share one) and
  CODEBUDDY_BASH_AUTO_BACKGROUND_DISABLED=1 (CodeBuddy's PowerShell
  tool moves timed-out commands to the background and only honors that
  variable). Both are appended last like the existing background-task
  disable so a caller-provided value cannot outvote them.

Fixes #9200

Generated-by: GLM-5.3-Flash (ZCode)

* fix(agent): keep CodeBuddy running after recoverable errors

MUL-7983

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

---------

Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-13 13:54:24 +08:00
Multica Eveandmultica-agent 432eb8eda7 fix(skills): restore legacy issue skill redirect for pre-merge daemons (MUL-7985) (#9213)
Partially reverts #9046. The backend again ships the
multica-working-on-issues redirect stub to daemons that do not
advertise platform-skill-v1, on both inline and skill-ref claims and in
bundle resolution. Released daemons (e.g. v0.4.39) hardcode that skill
name in their brief, and a backend deploy cannot rewrite it.

The daemon-side cleanup from #9046 is kept: current daemons advertise
platform-skill-v1 and point only at multica-platform.

Co-authored-by: multica-agent <[email protected]>
2026-10-13 12:42:39 +08:00
Multica Eveandmultica-agent b115ec80f1 MUL-7851: fix(daemon): don't count host sleep as idle-watchdog silence (#9201)
* fix(daemon): don't count host sleep as agent silence in idle watchdog (#9024)

The idle watchdog stamped activity in wall-clock unix nanos, so a laptop
that slept past the idle window woke up with every in-flight run already
over budget and force-stopped them all on the first tick.

Detect host suspend from the wall-clock gap between watchdog ticks and
credit that time back to lastActivityAt before each check, clamped to now.
Works the same on macOS/Linux (monotonic clock pauses in sleep) and
Windows (monotonic clock keeps counting), and covers the tool-in-flight
budget too since both read the same stamp.

MUL-7851

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

* fix(daemon): measure idle watchdog on an awake-only clock (#9024)

Review on #9201 showed the tick-gap heuristic still charges sleep to the
agent: a sleep shorter than one tick is never credited, and when the
overdue tick fires on wake (Windows) only gap−tick is credited.

Replace it with pkg/awakeclock, a clock that only advances while the host
is awake: Go's monotonic clock on Linux/macOS (already stops in suspend),
minus host sleep on Windows (GetTickCount64 − QueryUnbiasedInterruptTime).
lastActivityAt, the drain loop stamp, and Cursor's native tool activity
(Session.ToolActivity now returns an awakeclock reading) all share it.

Regression tests cover both review boundaries plus a long sleep, each with
a wall-clock control that proves the scenario reproduces the bug.

MUL-7851

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

---------

Co-authored-by: multica-agent <[email protected]>
2026-10-12 17:19:58 +08:00
804646324e MUL-7937 fix(realtime): guard client send channel teardown (#9145)
* test(realtime): reproduce send on closed channel during teardown

* fix(realtime): guard client send channel teardown

* test(realtime): make slow-client teardown regression deterministic

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

---------

Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-12 16:37:42 +08:00
3d021489f4 MUL-7697: fix(daemon): keep host TMP/TEMP for every Windows task (#9197)
* fix(daemon): keep host TMP/TEMP for every Windows task

Git Bash's /tmp usertemp mount is shared by all live MSYS processes of a
user, whichever runtime started them. #8985 stopped Windows Claude tasks
from seeding it with a task-private dir, but any other provider still
could, and later tasks then lost /tmp once that dir was deleted.

Apply the rule to every provider on Windows: TMP/TEMP inherit the host
temp, TMPDIR stays task-private, and Claude keeps its CLAUDE_CODE_TMPDIR
default. The Git Bash integration test now alternates providers.

Closes #8824

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

* test(daemon): expect inherited TMP/TEMP on Windows in env test

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

---------

Co-authored-by: multica-eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-12 15:56:39 +08:00
makesomethingshit eb59a69722 fix(daemon): preserve host temp for Windows Claude Git Bash (#8985) 2026-10-12 15:08:18 +08:00
8a2f518d54 fix(agent): MUL-7781 / MUL-7793 interrupt Codex turns after repeated Guardian 429s (#8934)
* fix(agent): stop Codex turns after repeated Guardian 429s

* fix(agent): recognize Codex Guardian rate-limit events

Use the app-server wire method and preserve semantic activity. Distinguish reviewer failures from policy denials, deduplicate recent reviews, and explain manual recovery after interruption.

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

---------

Co-authored-by: Yin <[email protected]>
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-12 14:18:02 +08:00
92d0b6666d MUL-7812: fix(agent): deliver MCP servers to Pi tasks (#9063)
* fix(agent): deliver MCP servers to Pi tasks

Pi skipped untrusted project config in JSON mode, and Multica never wrote .pi/mcp.json, so composed MCP servers never reached the runtime.

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

* fix(agent): hand Pi its MCP servers through a temporary extension

Writing .pi/mcp.json and passing --approve trusted the whole project directory and failed when that file already existed. An explicit --extension loads the servers without project trust.

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

* docs: mark Pi as reading agent MCP configuration

The provider comparison still said Pi skipped the MCP field. Pi now receives that configuration.

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

* fix(agent): harden Pi MCP registration

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

* fix(agent): allow empty Pi MCP configuration

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

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-12 13:26:00 +08:00
756cdd915c MUL-7794 fix(wakeups): surface recoverable PR condition diagnostics [ZIC-302] (#9066)
* fix(wakeups): surface recoverable PR condition diagnostics

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

* fix(wakeups): preserve condition diagnostic ownership

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

* test(wakeups): isolate receipt cleanup limit

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

---------

Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Yushen <[email protected]>
2026-10-12 13:15:46 +08:00
2penheimerandCursor 41c9bbafec fix(projects): preserve omitted fields in concurrent updates (#9111)
Co-authored-by: Cursor <[email protected]>
2026-10-12 12:47:28 +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
Basuotian 3647ec9a9c MUL-7687 fix(wecom): push an issue notification through the acting agent's bot (#8803)
In a multi-bot organization, every issue notification a member received arrived
through whichever WeCom bot they had bound most recently, because
FindChannelBindingForMember keys on the recipient alone. Three agents' comments
all looked like they came from one of them, and the apparent sender drifted
whenever the member re-bound an account.

An inbox item already carries the actor that raised it. When that actor is an
agent, the WeCom push now resolves the bot as the binding the recipient holds
on the acting agent's active installation
(FindChannelBindingForMemberOnAgentInstallation), keeping the recipient-wide
lookup as the fallback — for an agent this member never linked, and for
member-authored notifications. Delivery is never narrowed, which is the
guarantee #7136 named.

When the preferred bot provably put nothing on the wire, the recipient-wide
binding is tried too. Proving that needed a four-state relay verdict
(relayNotWired / relayAccepted / relayNoHolder / relayUncertain) in place of a
bool, plus a non-fencing DedupeStore.ClaimState read so the publisher can
observe a delivery claim without fencing a worker that is mid-delivery. An
unclaimed frame is fenced under a token no delivery worker ever presents, so a
late or replaying dispatcher cannot add a second card after the fallback ran.
An uncertain send is deliberately not retried.

The acceptance wait runs on the request's own goroutine, so it is paid only on
the agent-attributed attempt — the one with a second route behind it — and is
bounded by the caller's context rather than by the window alone.

Reviewed-by: seacen
Reviewed-by: Eve (MUL-7687)
2026-10-12 11:32:52 +08:00
xjh a820c9a0ef fix(task): preserve autopilot attribution after run completion (#9158) 2026-10-10 18:24:05 -07: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