mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-10 02:16:45 +08:00
d276991c603e6041d20fb5c1e65df8756d35d2ae
255
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d276991c60 |
MUL-6743 refactor(issues): remove the assignment handoff note (#7626)
* refactor(issues): remove the assignment handoff note (MUL-6743) Usage data puts the handoff note at 0.2% of assignments, so the box occupied the confirm dialog's centre for the other 99.8% and bought them nothing. Removing it takes the whole feature with it: the note input and its runtime version gate, the `handoff_note` write field on PATCH/batch issue updates, the task column write, and the daemon's opening-prompt + issue_context.md handoff sections. The confirm dialog itself stays — it still names who is about to run and still offers "Don't start yet" (suppress_run). The `*WithHandoff` enqueue methods only ever varied by that note, so they become `EnqueueTaskForIssueByActor` / `EnqueueTaskForSquadLeaderByActor` — the actor-carrying variants autopilot already called with an empty note. `preview-trigger` drops `handoff_supported`, which nothing read after the gate moved client-side. `agent_task_queue.handoff_note` is deliberately left in place: it holds historical rows and this is the expand half of an expand-contract pair. Nothing reads or writes it now, so a follow-up migration can drop it once no deployed binary selects it. Co-authored-by: multica-agent <[email protected]> * docs(issues): reshoot the assignment confirm dialog (MUL-6743) The asset still showed the handoff note box the surrounding prose no longer describes. Recaptured from the app at the same 600x450 frame; the confirm button now carries the send-chord keycaps it has shipped with since MUL-5694. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d2bc574ff2 |
MUL-6987: docs: document task runtime environment contract (#7956)
Document the daemon-injected task runtime environment contract, stability boundary, attribution behavior, and safe child-process token handling across English, Chinese, Japanese, and Korean docs. |
||
|
|
2c478897c7 |
docs(providers): mark Oh-My-Pi as Multica-managed MCP (MUL-6997) (#7965)
The comparison table still showed `—` in the "Multica-managed MCP" column for Oh-My-Pi (`omp`) across all four locales. That has been stale since #7565, which added `omp` to `MCP_SUPPORTED_PROVIDERS` and materialises the managed config as `{workDir}/.omp/mcp.json` via `prepareOmpMcpConfig`. The MCP configuration section further down the same pages was already correct — it lists only Antigravity, GitHub Copilot CLI, DevEco Code, and Pi as runtimes that do not read an agent's MCP config — so the table row was contradicting the prose on its own page. Every other row in that column was re-checked against `MCP_SUPPORTED_PROVIDERS` in `packages/core/agents/mcp-support.ts`; `omp` was the only mismatch. Co-authored-by: multica-agent <[email protected]> |
||
|
|
d4a712abf3 |
MUL-6771 feat(cli): filter and sort issue list by custom property (#7664)
Expose the `properties=` filter and `sort=property:<id>` that GET /api/issues already implemented but the CLI never surfaced, so scripts no longer have to page through everything and filter locally: multica issue list --property "Impact=High" --property "Impact=Medium" multica issue list --property "QA Status=__none__" --status in_review multica issue list --sort property:Impact --direction desc --property takes one Name=Value per flag; repeating a property ORs its values, different properties AND. Values resolve by name the way the rest of the property CLI works. __none__ is the reserved unset filter. A value the server could never have stored — a non-http(s) url, over-long text, a non-numeric number, a date in another format — is rejected up front rather than returning the empty page that reads like a real answer. The >=, <= and != spellings are reserved so comparison filters can be added later without breaking scripts. Also fixes propertySortExpr, which sorted a select property by the raw stored value — the option-id UUID, an arbitrary order. Values now rank by their position in the definition's option list, so an ordinal scale sorts by meaning (Low < Medium < High), matching what board grouping already treats as canonical. Closes #7663 |
||
|
|
a2d819ea40 |
MUL-6962 fix(daemon): give environment preparation failures their own failure reason (#7915)
* fix(daemon): give environment preparation failures their own failure reason A failed execenv.Prepare / Reuse is a filesystem error on the daemon's own host, but handleTask routed it through taskfailure.Classify — a classifier written to read agent and provider output — so a full disk, a denied permission or a directory held open by another process all landed in agent_error.*: the catchall today, provider_server_error on the report that opened the issue and sent hours of diagnosis at an LLM vendor. The daemon now tags those errors structurally (environmentSetupError) and reports the new platform-side reason environment_prepare_failed. The four existing sentinels still win: task identity, the prepare budget, skill downloads and the OpenClaw CLI deadline each name a specific cause inside the same phase. Installed daemons update on their own cadence, and they report a non-empty reason that FailTask deliberately leaves alone, so NormalizeDaemonReason upgrades any agent_error.* value whose error text opens with the prepare / reuse wrapper. That witness proves the run died before an agent existed, so every label in that namespace is wrong by construction. Not retryable and resume-safe: a full disk reproduces identically on the next attempt, and no session was touched. Closes #7913 Co-authored-by: multica-agent <[email protected]> * fix(mobile): parse failure_reason as an open string, and match copy to real scope Review follow-up on #7915. AgentTaskSchema pinned failure_reason to the six pre-MUL-1949 coarse values with `.catch("")`, so every refined reason was erased to undefined before any surface read it. run-row's badge map was widened in MUL-5370 and has been unreachable ever since: agent_error.*, skill_bundle_unavailable and the new environment_prepare_failed all rendered a bare "Failed". The field is an open string on the wire (packages/core/types/agent.ts) and in the chat message schema beside it; the task schema now matches, and the badge map moved to lib/run-failure-badge.ts so mobile's node-only vitest lane can cover the lookup a component test cannot reach. execenv.Prepare is not only filesystem work — it also writes and validates the per-provider local config (Codex home, Hermes overlay, Cursor MCP, and an OpenClaw config it fails closed on), so a malformed local config lands on this reason too. The Go docs, the chat copy in four locales and the docs table said "disk, permission, directory in use", which would send the reader past half the real causes — the same misdirection this reason exists to end. They now point at the raw error first and name the config case, with regression tests for a non-filesystem preparation failure on both the daemon and the legacy wire path. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
310cfdc1e5 |
MUL-6896 fix(i18n): finish the MUL-5703 glossary rollout across ja/ko and docs (#7867)
* fix(i18n): finish the MUL-5703 glossary rollout across ja/ko and docs (MUL-6896)
MUL-5703 settled the rule — the issue entity is translated (任务 / タスク /
태스크) and the agent run keeps `task` / 作業 / 작업 — but only ever landed in
the zh-Hans UI. Everything else drifted:
- ja/ko shipped 39 UI strings that still spelled the entity in English
(`issue`), or as イシュー / 課題 / 이슈.
- zh and ja spelled the *run* with the *issue* word in the project worktree
copy and in the MCP delete dialog — the exact collision MUL-5703 exists to
prevent. ko already had this right (작업).
- ko wrote runtime-health "issues" as 이슈, so a machine card read as the
entity. zh/ja use 异常 / 問題, and ko's own health_clear already said 문제.
- `conventions.ja.mdx` / `conventions.ko.mdx` still documented the *pre*-
MUL-5703 rule ("issue stays English"), contradicting the en/zh glossary.
- The docs prose sweep the glossary calls "being swept separately" never
happened: ~471 English `issue` in zh, ~414 イシュー in ja, ~418 이슈 in ko.
Sweeping the entity into 任务 / タスク / 태스크 forces the run to move too —
zh docs wrote it 执行任务, ja 実行タスク, ko 실행 태스크, all of which collide
with the entity word once the entity lands there. So the run is rewritten to
`task` / 作業 / 작업 in the same pass, matching what each UI already shows.
Standalone labels keep the English source's capitalisation (`Task` page
title and links); inline mentions are lowercase, like `skill`.
Also corrects the en source for two squad strings: squads receive *issues*
and the leader delegates *sub-issues*, not "tasks" / "sub-tasks". The three
translations already said the entity, so only en moves.
Not touched: the landing changelog archive, the generic English noun "task"
in chat/onboarding marketing copy, `issue` in the "problem" sense, and the
unrelated structural drift in the ja/ko conventions pages (they are missing
the "Runtime display names" section from an older sync).
Verified: 4907 views tests pass including locale parity, typecheck and lint
clean, go build plus the integrations/cli suites pass, and no doc anchor
broke (checked before/after).
Co-authored-by: multica-agent <[email protected]>
* fix(docs): sweep the last old run-word leftovers in zh copy (MUL-6896)
Review caught the docs home page still linking "[执行任务](/tasks)" while ja/ko
had already moved to 作業 / 작업. Four more of the same class turned up next to
it: the same link in README.zh, "任务生命周期" in VISION.zh (en: "the task
lifecycle"), and two redundant "task(task)" glosses the sweep produced where
the source had written "执行任务(task)".
With these, `执行任务` / `実行タスク` / `실행 태스크` are gone repo-wide, so the
"prose now follows the table" line added to conventions.mdx is true.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
61ea48fd2e |
MUL-6881 fix(daemon): keep one worktree branch per conversation in local worktree mode (#7815)
* fix(daemon): keep one worktree branch per conversation in local worktree mode Every comment on a local_directory issue started a new task, and worktree mode gave each one a branch of its own forked from the user's HEAD. Turn two therefore stood in a tree that did not contain turn one's work, with that work stranded on another branch and nothing saying so. Tasks that share a conversation — the turns of one issue, one chat session — now share a branch, agent/<agent>/<issue>, and a follow-up checks it out at its tip instead of forking from HEAD. Env roots stay per task, so sibling tasks still run concurrently; when one already holds the conversation's branch, the other forks from its tip onto agent/<agent>/<issue>-<task> rather than falling back to HEAD. Replaying the user's uncommitted tree onto a continued branch cannot use the turn-one path: that merge takes the user's HEAD as its base, so it conflicts exactly when the agent edited the lines the user had left uncommitted. Each turn now records the snapshot it saw at refs/multica/local-state/<branch> and the next one replays only the difference — a cherry-pick whose parent is that snapshot. A genuine conflict leaves the branch as it stands and logs which files did not come across, rather than stranding the conversation. Teardown follows the same rule everywhere: a branch this prepare did not create belongs to the turns before it, so Finalize's read-only cleanup, Discard and the prepare rollbacks only ever drop a branch they created themselves. Co-authored-by: multica-agent <[email protected]> * fix(daemon): prove branch ownership and hand replay conflicts to the agent Review of the per-conversation worktree branch found four ways it could put the wrong tree in front of the agent. All four came from the same two gaps: the user's snapshot did not describe their whole directory, and a branch was recognised by its name. The snapshot is now one commit covering tracked edits AND untracked files, built in a private index inside the task's env root. It answers the question a continued branch actually has to ask — what has the user changed since the state I already carry — for every file, so a once-untracked file the agent edited is no longer reverted to the user's older copy each turn, and their later deletion of one carries. Redirecting GIT_INDEX_FILE also removes the .git/index.lock race the previous capture had to retry around (#7434). A conflict between the user's newer edits and the branch's own work is now the agent's to resolve, per the product decision on the issue: the worktree is handed over mid-merge, the turn's prompt names the files and says which side is which, and Finalize refuses to deliver — preserving the worktree — while anything is still unmerged. The snapshot only advances once the branch really carries it, so an unresolved edit is offered again next turn instead of being recorded as synced. Before this, the conflict was reset away and the user's change was lost from every later turn. Continuing a branch is decided by an ownership record — workspace, agent and conversation ids, recorded in the snapshot commit at refs/multica/local-state/<branch> — never by the branch name. A same-named branch the user created is left untouched and the task takes agent/<agent>/<issue>-<fingerprint>, which its own follow-ups then continue. The same check keeps two workspaces that mint one issue identifier apart. Those refs are now GC'd: every prepare drops the record of a branch that no longer exists, and the docs say where they live and how to clear them. Co-authored-by: multica-agent <[email protected]> * fix(daemon): tie a task branch's record to the branch, and quote untrusted paths Ownership proved that Multica once wrote a branch of this name, which stayed true after the user deleted it and created their own under the same name. No prepare runs in that gap, so the orphan sweep never sees the branch missing and the next turn continued straight into the user's unrelated work. The record now also pins the branch tip it was written at, as the second parent of the record commit, and a branch is only continued while that commit is still an ancestor of its tip. Deleted-and-recreated, force-moved and rebased branches all fail that test; committing on top of a delivered branch — the normal case — passes it and picks the user's commit up. Finalize re-records afterwards, because a checkpoint written at prepare time sits on the branch's base, which is exactly where a recreated branch would sit. Conflicted paths reaching the turn's prompt are quoted rather than wrapped in a code span. They come from the user's repository and git allows newlines, backticks and quotes in a filename, so a raw one could close its list item and continue as an instruction line of its own; the same quoting is applied to the Finalize error. The list is bounded at 50 names with the remainder counted. Also drops a duplicated pair of bullets in the Chinese docs and corrects two file comments that still described the stash-and-copy replay. Co-authored-by: multica-agent <[email protected]> * fix(daemon): make the branch record part of delivery, and bound the conflict list by bytes The record could still authorise a branch that was not ours. Finalize wrote it after removing the worktree and treated a failure as non-fatal, so a failed update-ref left the ref pointing at the record Prepare had written — whose checkpoint is the branch's base, which is exactly where a branch the user deletes and recreates from HEAD sits. The ancestor check then passed on their work. Two changes close it. A checkpoint is only written when it is a commit the branch could not sit at without this conversation's work: Finalize records the tip it actually delivered, and Prepare records early only once the branch has moved off the user's HEAD — which is the baseline commit, or a continued branch's own tip. And the record is now part of delivering: Finalize writes it BEFORE removing the worktree and fails the task if it cannot, so the worktree is still there to preserve, exactly as for a commit that could not be made. The checkpoint is the commit the caller delivered rather than a re-read of the branch ref, so a branch moved in that window fails the next ancestor check instead of being recorded as current. A sibling racing the very first turn of a conversation on a clean tree now finds no record yet and starts its own line of work rather than forking from a branch it cannot prove is ours. That is the price of the rule and it is the safe side. The conflict list in the prompt is bounded by rendered bytes rather than by entries — a git path is as long as the filesystem allows, so 50 entries bounded nothing — with the remainder counted and a single oversized path unable to overrun the budget either. Co-authored-by: multica-agent <[email protected]> * fix(daemon): give every task branch a commit of its own, and verify what a run delivers The checkpoint rule — record only a commit the branch could not sit at without this conversation's work — held in Prepare and not in Finalize. Finalize took whatever the worktree's HEAD was, so a run that reset its worktree back to the user's own HEAD had that plain user commit recorded as the branch's checkpoint, and a branch the user later recreated there passed the ownership check. Two changes make the rule total. Every branch this mode creates now gets a baseline commit of its own, empty when the user's tree was clean: without it the branch sits exactly where the user's HEAD does and nothing tells it apart from a branch they create at the same place. It also makes `git diff <baseline>.. <branch>` the agent's work on every branch rather than only on the ones that started dirty, which is what the surrounding comments already claimed. And Finalize verifies the delivery point before recording it: the tip has to BE the task's branch — not a detached checkout, not a branch someone moved underneath the run — and it has to still contain the anchor, which is that baseline or the checkpoint the previous turn recorded and this one verified. A delivery that fails either test is not recorded at all: the task fails, the worktree is preserved, and the error names what is missing. A sibling racing the first turn of a conversation forks from it again, since every branch now carries a recordable checkpoint from the moment it is prepared. Co-authored-by: multica-agent <[email protected]> * fix(daemon): keep the anchor on the commit that carries this turn's user edits A follow-up turn commits the user's newest edits as its own baseline, but the anchor stayed on the checkpoint the previous turn had recorded. A run that then reset its worktree back to the previous tip still passed verification — the old checkpoint is an ancestor of it — so the turn was recorded as having delivered edits the branch no longer had. The turn after that computed its replay from a state the branch never took, and the user's edits were simply absent from the worktree with nothing to indicate it. The anchor now follows every baseline this task commits, so it always names the newest commit carrying the state about to be recorded. Resetting past it fails the turn and preserves the worktree, as for the first-turn case. The conflict path had the same hole from the other end: there the branch does not carry the snapshot until the agent resolves and something is committed. When a turn that started mid-merge ends with the branch exactly where it began, the record keeps the state the branch is known to carry instead of advancing, and the next turn offers those edits again. A resolution in favour of the branch's own side leaves the same empty trace as discarding the merge, so this trades a merge that may have to be concluded twice against recording edits as delivered that may have been dropped. Co-authored-by: multica-agent <[email protected]> * fix(daemon): measure a delivery against the commit its own turn started from A turn that starts mid-merge only carries the user's edits once the agent resolves and something is committed, and the guard for that compared the delivered tip against the checkpoint an earlier turn had recorded. Those are not the same commit: the user committing on a delivered branch is supported and tested, so the branch tip a later turn starts from can already be past that checkpoint. A resolution that committed nothing then left the tip equal to the turn's own starting point but not to the older checkpoint, the guard did not fire, and the edits were recorded as delivered — missing from the turn after with nothing to indicate it. Both questions now measure against LocalWorktree.BaseCommit, the commit this turn actually started from. It is at least as new as the earlier checkpoint in every case, so the separate anchor field is gone: a delivered tip that contains this turn's starting point contains everything an earlier turn recorded, and a tip that does not is a run that reset away work already delivered. Co-authored-by: multica-agent <[email protected]> * test(daemon): cover the claim-to-branch wiring with a two-turn Prepare test Every multi-turn test so far drove PrepareLocalWorktree with hand-filled params, which skips the step that turns a claim into them: which field becomes the branch name, and which become the identity the branch is recorded under. This one runs two turns of one issue through Prepare itself, and a third for another workspace carrying the same issue identifier. Both halves of that mapping are load-bearing — blanking the conversation key or fixing the workspace id makes it fail. Co-authored-by: multica-agent <[email protected]> * fix(daemon): carry the worktree's own state across the preparation helper Prepare runs in a short-lived helper process and the Environment it built comes back to the daemon as JSON. LocalWorktree keeps everything Finalize reasons about in unexported fields, and ordinary struct marshalling drops those without a word — so the daemon finalized a worktree whose owner, snapshot, tracksState and createdBranch were all zero. Nothing failed; the guarantees were simply absent in production. No delivery verification ran, Finalize recorded nothing (leaving the checkpoint at the baseline Prepare had written inside the helper), a conflicted turn could not hold its snapshot back, and a read-only turn kept its branch. Every in-process test passed throughout, because they call PrepareLocalWorktree directly. Custom Marshal/Unmarshal now carry that state. aborted stays out of the wire deliberately: the daemon sets it after Prepare returns, so it belongs to the parent. The regression tests run two turns through PrepareIsolated — the real boundary — and assert the state arrives, that Finalize's record names the tip it delivered rather than the baseline, and that a read-only turn still drops its branch. Found by running the whole stack: a real daemon, a real local_directory in worktree mode, and two real turns. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
2ebe0ae327 |
MUL-6862: fix(codex): widen thread setup handshake budget (#7790)
* fix(codex): widen thread setup handshake budget * fix(codex): preserve handshake timeout overrides |
||
|
|
1dd6b9ecdb |
MUL-6570 feat(wecom): route a reply to the replica that holds the bot's socket (#7429)
WeCom is the only channel with no outbound REST path: every write goes over the aibot WebSocket, and the WS lease means exactly one replica holds it. `chat:done` is published on the in-process bus by whichever replica served the daemon's completion — a load-balancer decision, unrelated to the lease. Off-lease, the reply was dropped (#7215, #6890), and the documented workaround was to run the WeCom-enabled backend single-replica. This routes the reply over the Redis Stream relay the repository already runs for the same shape of problem, so the replica holding the socket sends it. - At-most-once across replicas and restarts via a Redis claim keyed on the turn, with a TTL derived from the relay's `ReplayGrace` rather than a constant. - Delivery runs on a bounded dispatcher, never on the shared shard read loop, with per-installation lines that keep two answers to the same person in order even across a re-offer. - Attachments and `inbox:new` pushes are routed too, not just reply text. - Every routed reply gets a final outcome, settled exactly once by the replica that routed it: `outbound_dropped_total{reason="no_live_connection"}` when no replica took the delivery. The residual window — every replica mid-reconnect — remains, but is now measurable instead of silent. Multi-replica WeCom is supported in sharded/dual relay mode; legacy mode and deployments without Redis keep the previous behaviour and the single-replica requirement. `SELF_HOSTING.md` and the four `channels*.mdx` docs state the split. |
||
|
|
fe59262188 |
feat(agent): add Huawei Cloud CodeArts support (#6985)
* feat(agent): add Huawei Cloud CodeArts support * fix(migrations): resolve CodeArts prefix collision * fix(agent): address CodeArts review feedback * fix(agent): tighten CodeArts integration scope * fix(agent): resolve CodeArts follow-up review blockers * fix(agent): redact CodeArts launch arguments * docs: sync supported runtime counts * fix(agent): own CodeArts model probe process tree |
||
|
|
64ec7f5416 |
MUL-6737: fix(timeouts): close the two gaps the 2h inactivity budget left open (#7699)
* fix(timeouts): close the two gaps the 2h inactivity budget left open (MUL-6737) Two independent places where a timeout still answered the wrong question. Codex runs a semantic-inactivity timer inside the app-server protocol, and unlike the daemon's watchdog it cannot see that a tool is in flight: a commandExecution that prints nothing for the window trips it anyway. At a fixed 10m ceiling that timer killed a quiet test suite or an output-buffering `docker build` long before the 2h daemon budget meant to protect it, which made "we raised the budget to 2h" false for Codex users. It now derives from the daemon budget, taking the larger of the idle and tool windows because it stands in for both. A tool budget of 0 means "never force-stop during a tool", which this timer cannot express, so it falls back to the idle budget rather than running unbounded; with the watchdog suite disabled entirely Codex keeps its own built-in default, as it always has. The env override and flag are unchanged. Queued tasks expired on a pure wall clock: queued longer than a TTL meant failed. That conflated "nobody is coming for this" with "the queue ahead is long", and only the first is a failure. MUL-6558 was the second — a self-hosted runtime with low concurrency held its own queue past 2h and healthy work died as queued_expired. The TTL knob added then only moved the cliff. Expiry now keys on whether the owning runtime still proves liveness, the same signal FailTasksForOfflineRuntimes already uses for dispatched/running rows, so a daemon going down retires its queued and its in-flight work on one clock instead of two. A busy runtime keeps its backlog for as long as it takes. MULTICA_TASK_QUEUED_TTL is removed with the wall clock it configured. Heartbeat age is read directly instead of gating on runtime.status='online' so a row stuck at 'online' with a dead heartbeat still releases its queue, and rows with no usable runtime binding — runtime_id IS NULL from before migration 251, whose CHECK landed NOT VALID, or a runtime row that is gone — are expired on sight rather than stranded forever by a JOIN that matches nothing. Tests: the queued sweep test now runs both phases against the same rows, asserting the 5h-old task survives while its runtime heartbeats (the MUL-6558 regression) and only expires once that runtime goes silent. Assertions are scoped to the test's own task ids, because a runtime-keyed sweep legitimately also collects queued rows other tests left on the shared fixture runtime. Co-authored-by: multica-agent <[email protected]> * fix(sweeper): give a queued task its own reconnect grace before expiring it Review catch: keying expiry on runtime liveness alone was too aggressive in one direction. Enqueue binds a task to agent.runtime_id without checking that the runtime is up (task.go CreateAgentTask call sites), so for a runtime that has already been dark for longer than the grace, the liveness clause is satisfied the moment a new task is created — assigning an issue to a laptop closed overnight would fail it inside one 30s sweep tick instead of waiting for the machine to come back. That is the opposite of what "reconnect grace" promises. Require the row's own age as well: a queued task now waits a full grace, counted from when it started waiting, before it can be given up on. A heartbeating runtime still never expires its backlog, so the MUL-6558 fix is unchanged. The age bound also stops the sweep re-evaluating the runtime subquery against every queued row on every tick. Also remove the deploy surfaces the previous commit missed — the knob was deleted from the server but helm (values + configmap), the self-host compose file, and the CI-run helm render assertions still advertised it, so operators would have kept setting a variable nothing reads. Same for five orphaned comment lines in .env.example describing the removed variable. Correct the fail-closed comment on the unbound-runtime arms: they are not reachable for "rows predating migration 251" — runtime_id was NOT NULL from migration 004 until 251 replaced it with a CHECK that every insert and update is verified against, and the migration-004 FK is ON DELETE RESTRICT. The arms stay as defence against a future schema change, which is what the comment now says. Tests: a third phase covers the reported case — a task enqueued against an already-dead runtime survives, then expires once it has waited a full grace of its own. Phase 2's expectation is corrected accordingly: the row created at now() no longer expires when its runtime dies, because it has not waited its own grace yet. Co-authored-by: multica-agent <[email protected]> * docs(timeouts): state both queued-expiry conditions and correct the FK claim Review follow-up on |
||
|
|
dcc2d58251 |
MUL-6737: fix(daemon): raise the agent inactivity budget to 2h and derive the tool budget (#7685)
The idle watchdog was sized to notice a hang quickly (30m), but the two costs it trades between are not symmetric: force-stopping a healthy run throws away the work and flips the issue to blocked, while noticing a hang late only holds a concurrency slot. - DefaultAgentIdleWatchdog 30m -> 2h, sized by the longest legitimate silent step. - AgentToolWatchdog now derives from the idle budget in both directions; MULTICA_AGENT_TOOL_WATCHDOG still overrides and 0 keeps its 'never force-stop during a tool call' meaning. - Watchdog tick capped at 5m, so worst-case detection is budget + 5m instead of budget * 1.5. - Dropped the unreachable 30s tick floor (window >= 1m implies window/2 >= 30s) and asserted the property it was written for directly. - Removed the dead MULTICA_CODEX_TIMEOUT from .env.example; documented the generic watchdog and Codex timeout rows in CLI_AND_DAEMON.md; synced four doc locales. |
||
|
|
9a19c90f8e |
feat(skills): add Import from local to the New skill dialog (MUL-6703) (#7570)
Adds an "Import from local" route to the New skill dialog, so a member can pick a folder containing SKILL.md or a .skill / .zip archive and import it straight into the workspace. Shows name, description and file count before importing, and fails clearly on a missing SKILL.md, oversized input or a name conflict instead of creating a partial skill. Reuses the existing POST /api/skills/import endpoint; web and desktop share the entry point. Docs and locales updated for all four languages. Directory selection is decided from path + size before any file is read, so a large project folder cannot be pulled into memory. Concurrent selections are guarded by a generation counter, and the import response is parsed through a zod schema before its status is branched on. Co-authored-by: J <[email protected]> |
||
|
|
3b1018e9c9 |
Revert "MUL-6016: feat(task): retry exact Codex capacity failures (#6747)" (#7586)
This reverts commit
|
||
|
|
26c04803c8 |
MUL-6698: feat(agents): make conversation starters discoverable from the chat they run in (#7573)
* feat(agents): make conversation starters discoverable from the chat they run in
"Suggested first actions" named its content but never its surface, and the
chat empty state it drives gave no sign those buttons were configuration at
all. The link between the two existed only in the head of whoever built it.
Close the gap from both ends rather than explaining it:
- Rename to "Conversation starters" and rewrite the hint to name the surface
("shown when someone opens a new chat with this agent"). "Quick action" was
considered and rejected: it is already taken by chat follow-ups, the issue
sidebar and settings, and it promises execution where selecting a starter
only fills the composer.
- Add a "Customize starters" link to the empty state for anyone who may edit
that agent, deep-linking to its Instructions tab with the editor scrolled
into view and briefly ringed. Readers never see it.
- Preview the real empty state under the editor, rendered through the same
component and resolver the chat uses so it cannot drift — and label the
fallback explicitly, which is how an author learns the three suggestions
they never configured are defaults they can replace.
The permission rule is one pure function shared by both chat surfaces: no
link for a reader, an archived agent, or a backend that drops starter
prompts (its Instructions tab renders no editor to land on).
Co-authored-by: multica-agent <[email protected]>
* fix(agents): re-arm the starter-prompts deep link and sync localized docs
Two findings from review.
The focus latch was one-shot per mount, so it swallowed every click after
the first: a chat window left open beside the Instructions page could send
the same link again and nothing would happen — and the `focus` param would
be left stranded in the URL. It also blocked a link aimed at a DIFFERENT
agent when navigating between two agent pages on the same route. Re-arm the
latch when the param disappears and key it by agent id, keeping the guard
that stops a lagging URL update from looping. Both cases now have regression
tests, each verified to fail against the previous latch.
The zh/ja/ko copies of the agent docs still carried the old name and said
nothing about where the setting takes effect. Sync all six files with their
shipped UI translations and with the English additions (surface, preview,
Customize link).
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
1ef1d65bef | fix(cli): remove obsolete autopilot priority flag (#7557) | ||
|
|
8fda4f22d5 |
MUL-6016: feat(task): retry exact Codex capacity failures (#6747)
* docs: record Codex capacity retry design * docs: add Codex capacity retry implementation plan * fix(taskfailure): recognize exact Codex capacity error * feat(task): retry exact Codex capacity failures * feat(server): configure Codex capacity retries * docs: expose Codex capacity retry setting * fix(task): align capacity retry comments --------- Co-authored-by: cuijs <[email protected]> Co-authored-by: Claude <[email protected]> |
||
|
|
f8ec870f30 |
MUL-6629: feat(agents): add per-agent starter prompts for chat (#7261)
* feat(agents): add per-agent starter prompts Co-authored-by: Agent Op <[email protected]> * fix(agents): default omitted starter prompts Co-authored-by: Agent Op <[email protected]> * fix(migrations): renumber agent starter prompts Co-authored-by: Agent Op <[email protected]> * fix(chat): make starter prompt prefills reliable Co-authored-by: Agent Op <[email protected]> * fix(agents): preserve starter prompt writes Keep dirty starter-prompt edits across agent refetches and fail closed when an older server cannot persist them. Clarify that copied starter prompts have no dedicated CLI override. Co-authored-by: Agent Op <[email protected]> * fix(migrations): avoid starter prompt prefix collision Main now uses migration prefixes through 403, so move the starter-prompts migration to the next available prefix before requesting another review. Co-authored-by: Agent Op <[email protected]> * fix(agents): preserve edits after failed save Ignore optimistic instruction snapshots while a save is pending so a rejected update can roll back the cache without erasing the user's submitted starter prompts. Cover the delayed optimistic-update and rollback sequence for both label and prompt fields. Co-authored-by: Agent Op <[email protected]> --------- Co-authored-by: Agent Op <[email protected]> |
||
|
|
9e2d9b6d1c |
MUL-6661 feat(channels): add /new and /clear conversation controls (#7468)
* feat(channels): add new and clear conversation controls * fix(dingtalk): resolve bot identity before dispatch * feat(chat): expose channel-created chats consistently * refactor(channels): snapshot task delivery routes |
||
|
|
11ab949dc9 |
MUL-6611: docs(self-host): route /health through the proxy, add single-origin example and release-tag note (#7466)
* docs(self-host): route /health through the proxy, add single-origin example and release-tag note
Document the four traps we hit deploying twice from the quickstart alone,
across all four doc locales (en/zh/ja/ko):
- `multica setup` probes <server-url>/health and needs a 200, but the web
image does not proxy /health, so a proxy that forwards everything to the
frontend makes setup fail with "Server not reachable" against a healthy
stack. Add the explicit route to both Caddy examples, a new
troubleshooting section, and a hint at the CLI setup step.
- Add a single-origin reverse-proxy variant (one domain / one port), the
shape small servers actually use, with the daemon-critical paths called
out (/health, /ws).
- The published images track the newest release tag while a plain clone
checks out main. Anyone building the CLI or images from the checkout
should pin the tag first or binaries drift from the containers.
- Add an /api/config verification step: daemon_server_url must print the
public URL, and on Docker Compose < 2.24 FRONTEND_ORIGIN/MULTICA_APP_URL
do not follow FRONTEND_PORT (the ${FRONTEND_PORT} references copied from
.env.example stay literal, refs #6146).
- Note that a curl against /ws returning 400 is expected without a daemon
token; the backend log entry proves the proxy chain works.
* docs(self-host): review fixes — WS 400 attribution, section placement, grep over jq
- The WS-400 note attributed the rejection to daemon-token auth; the actual
reason a bare curl gets 400 is the missing workspace query parameters
(browsers auth by cookie, daemons auth in the first frame after upgrade).
Fixed in all four locales.
- The new troubleshooting section was inserted before the trailing two
paragraphs of the realtime section, swallowing them; moved it after the
reverse-proxy example line.
- daemon_server_url comes from MULTICA_PUBLIC_URL when set (otherwise
MULTICA_APP_URL) and is omitted when both are unset — the verification
note now says so, and the command uses grep instead of jq (jq is not in
the prerequisites list).
- zh: 问题排查 → 故障排查 to match the page title; ja: replaced —— with
the file's established " — " / parenthetical style; linked the matching
row in the Common issues tables to the new anchor in all locales.
* docs(self-host): route /api/daemon/ws in the single-origin example
Daemons dial {server-url}/api/daemon/ws for their long connection, not
/ws — and the web image cannot proxy WebSocket upgrades (which is why
/ws is routed to the backend directly in every example). The single-
origin variant added earlier only routed /ws, leaving the daemon's
handshake to fail through the frontend; the daemon then silently falls
back to polling, with repeated status=400 lines on that path in the
backend log as the only symptom. Add the explicit handle to the
single-origin example in all four locales, and a self-host note in the
troubleshooting WebSocket section.
* docs(self-host): address review — drop Compose version claim, qualify /health wording, bound the curl-400 check
- Remove the unverified 'Docker Compose < 2.24' cutoff; advise setting
FRONTEND_ORIGIN/MULTICA_APP_URL explicitly when /api/config prints
localhost.
- Qualify /health statements for merged #7467: current web releases
forward the path, older ones do not; keep the explicit direct route
(works across releases) and list DNS/TLS/firewall/timeout/5xx as
other probe failures. Covers the Caddy comment, the setup section,
the FAQ row, and the troubleshooting page in en/zh/ja/ko.
- A bare curl 400 on /ws only proves plain HTTP routing reaches the
backend (workspace-param rejection happens before the upgrader);
point at DevTools Network→WS for a real 101 handshake check.
- ja: 転達 → 転送; fix the stale 'Step 1' self-link anchors (en/zh/ja/ko).
* docs(self-host): document the real daemon_server_url precedence and omission
daemonSetupURLsFromEnv resolves the app URL as MULTICA_APP_URL falling
back to FRONTEND_ORIGIN, and omits both daemon fields from /api/config
when that chain is empty — even if MULTICA_PUBLIC_URL is set. The docs
said 'MULTICA_PUBLIC_URL, otherwise MULTICA_APP_URL; empty means
neither is set', which missed the fallback and mis-stated the omission
condition. State all three inputs and the actual condition in
en/zh/ja/ko.
* docs(self-host): include MULTICA_DAEMON_SERVER_URL in the daemon_server_url precedence
daemonSetupURLsFromEnv reads MULTICA_DAEMON_SERVER_URL before
MULTICA_PUBLIC_URL (server/internal/handler/config.go:128), so the
verification note in all four locales was one step short of the real
chain. environment-variables.mdx already documents it in full.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
c8154f9d1a | feat(selfhost): add daemon server URL override (#7331) | ||
|
|
8a5a6adbf7 |
feat(agent): add ZeroClaw as a native ACP runtime (MUL-6511) (#7351)
* feat(agent): add ZeroClaw as a generic ACP agent runtime
Add ZeroClaw (a Rust-based, single-binary generic agent CLI) as a
first-party runtime, driven over the standard ACP JSON-RPC transport via
`zeroclaw acp`, mirroring the Dim/QwenPaw/Grok ACP backend shape. Only the
binary, launch args, and blocked-flags policy are ZeroClaw-specific; the
backend reuses the shared hermesClient ACP transport and deliverable
tracker.
No provider-specific quirks (permission-preset injection, minimum-version
gate, or a separate authenticate step) are assumed without evidence — those
were verified against real Dim/Grok binaries and would need the same
hands-on verification against a real ZeroClaw binary before being added
here.
Backend: SupportedTypes/New/launchHeaders wiring, model discovery via
session/new's advertised catalog, daemon probe with
MULTICA_ZEROCLAW_PATH/MODEL overrides, AGENTS.md context delivery,
default-command-name registration, metrics label, and a migration widening
the runtime_profile.protocol_family whitelist.
Frontend: provider union, MCP-config support flag, and a placeholder
provider-logo mark (no official ZeroClaw brand asset exists yet).
Tests mirror the existing ACP backend conventions: fresh-session,
resume-via-session/load, resume-not-found, transient-load-error, blocked
launch args, timeout, and the shared ACP deliverable-boundary regression
suite.
* fix(agent): renumber ZeroClaw's migration to 378 to avoid a same-day collision
Both this branch and a concurrently-implemented Junie runtime branch
independently picked migration 377 off the same origin/main tip.
Renumbering ZeroClaw to 378 up front avoids opening two PRs from the
same author that claim the same migration number.
* fix(agent): align the ZeroClaw ACP backend with the real 0.8.4 runtime
The backend was written against an assumed vanilla ACP handshake and never
checked against a ZeroClaw binary. Driving a real zeroclaw 0.8.4 over stdio
shows five of those assumptions are wrong, and the test fake encoded the same
assumptions, so the suite stayed green against a contract the runtime does not
have.
- Drop the session/set_model request. It is not in ZeroClaw's dispatch table
(-32601) and no ACP handler reads a model param at all, so it could never
succeed; because its failure was fatal, any agent with a model configured
failed before session/prompt was reached. The model comes from the ZeroClaw
agent profile, so opt zeroclaw out of ModelSelectionSupported and stop
reading MULTICA_ZEROCLAW_MODEL rather than leaving the UI offering an
override that is silently ignored.
- Label token usage from initialize's _meta.zeroclaw.defaultModel, the only
model identity the ACP surface exposes. session/new returns just
{sessionId, workspaceDir}, so the two extractACPCurrentModelID reads were
dead code and the model-catalog discovery they fed could only ever return an
empty fallback while paying an ACP subprocess per catalog read. Return an
empty catalog directly, as qwenpaw/mcode do.
- Resume through session/resume instead of session/load. Both restore the
transcript into the agent, but load also replays every retained message back
as session/update notifications, which fed the previous answer into this
turn's deliverable. Add the acceptNotification turn gate every other ACP
backend carries, so out-of-turn chunks are dropped even if a runtime flushes
them early (#1997).
- Send agentAlias on session/new when the operator names one. ZeroClaw
auto-selects an agent only when its config holds exactly one entry, so
multi-agent installs failed with -32602 and had no way out: `zeroclaw acp`
has no --agent flag, and passing one kills the process at argument parsing.
The alias is consumed from custom_args and travels as a param; it is omitted
when unset, because a hardcoded guess breaks the single-agent case that
works today. The refusal now names both remedies.
- Stop advertising MCP support. No ZeroClaw handler reads params.mcpServers;
acp_enable_mcp gates that agent's own mcp_bundles, not the client list, so
MCP is operator-side only. Hide the MCP tab and warn on a stale mcp_config
instead of failing the task.
Also teach isACPSessionNotFound about ZeroClaw's custom -32000
SESSION_NOT_FOUND, without which a stale session id was retried forever
instead of restarting fresh. The wording check still decides, so a transient
error under the same code stays non-rejecting.
Tests now encode the observed handshake — _meta default model, session/new
without a catalog, -32000 for an unknown session, -32601 for set_model — and
cover the replayed-history, alias and MCP paths.
* fix(agent): make ZeroClaw integration merge-safe
Co-authored-by: multica-agent <[email protected]>
* fix(agent): tighten ZeroClaw ACP fallbacks
Co-authored-by: multica-agent <[email protected]>
* fix(agent): preserve ZeroClaw retry semantics
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
a4e1ea7140 |
MUL-6581: expose globally versioned Plugin Public API (#7446)
* feat(plugins): expose globally versioned public API Co-authored-by: multica-agent <[email protected]> * fix(plugins): proxy same-origin public API Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
8d2da6b8db |
MUL-6479 feat(dev): make a local environment a named object with one verb per lifecycle step (#7325)
* MUL-6479 feat(dev): make a local environment a named object with one verb per lifecycle step Local development had six entry points, thirty-four make targets and no way to see or delete what they created. `make up` / `status` / `list` / `down` / `destroy` / `gc` / `env-exec` replace that with one set of verbs, shared by humans and agents, with `C=api,web,daemon,desktop` selecting components. Three defects made the old flow untrustworthy rather than merely verbose. 1. The database the tooling created was not the one the application reached. `ensure-postgres.sh` creates through `docker exec`; when a native PostgreSQL owns 5432 the container never binds the host port, so the create lands in a server the backend never talks to. It printed "✓ PostgreSQL ready" and the next step died with `SQLSTATE 3D000`. On the machine this was written on the two servers had already diverged: 94 same-named databases, 69 more in the container with no counterpart. `up` now creates through DATABASE_URL and treats a successful `migrate up` — which pings that same string — as the only proof; failure diagnoses the port owner instead of reporting a missing database. 2. Identity was computed, not allocated. Ports, database names and CLI profiles all came from `cksum($PWD) % 1000`, and a collision was silent: the loser fails to bind while the winner keeps answering 200. Allocation now happens under a lock, probing from the same path hash so a checkout keeps its familiar numbers, and requires the slot to be both unregistered and free of a live listener. The manifest in ~/.multica/dev/envs/<name>/ is what every later command reads. 3. Creating an environment left no record of how to destroy it, so there was no destroy — only archaeology. `down` stops processes and keeps the data; `destroy` consumes the manifest: processes, database, profile, slot. `list` makes leaks visible and `gc` collects environments whose checkout is gone or whose TTL passed. /health now reports pid, commit and started_at. A 200 alone proves something is listening, which is exactly what let a failed restart keep serving the previous build; `up` compares started_at against its own launch and refuses an answer that predates it. Two constraints are enforced rather than documented: the daemon is started from a built server/bin/multica, never `go run` (the daemon re-execs its own path for every task, and `go run` deletes that binary when the launcher exits), and `C=daemon` under a .multica/daemon_task_context.json marker stops with that explanation up front instead of spending a login on a refusal. scripts/dev-env.test.sh covers the registry verbs against a throwaway MULTICA_DEV_HOME with no services running, and pins the regression that made `down` exit 1 after reporting success: on bash 3.2 a command substitution whose function ends in a failing command aborts the script under `set -e`, and "no process is listening" is that function's normal answer. `make dev` is unchanged and still runs in the foreground. Co-authored-by: multica-agent <[email protected]> * fix(dev): close local environment lifecycle gaps Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
bedad9e222 |
MUL-6485: isolate hosted plugin surfaces (#7329)
* feat(plugins): host plugin artifacts, bind installs to immutable versions A plugin used to be a URL. The manifest was frozen at install, but the surface script was fetched from the author's server every time a panel opened, with no integrity check — so an administrator consented to a manifest while the browser ran whatever that host served that day, inside the scopes already granted. The author's uptime was our uptime, every panel open leaked the reader's IP and "who read which issue" to the author, and publishing at all required a public HTTPS domain, which is why the repo's own deploy-sentinel example never ran. The author now uploads an artifact bundle and Multica stores it: - plugin_package / plugin_package_version / plugin_package_file. A version is insert-only, and the (package_id, version) unique index is what makes immutability a database rule rather than a convention. - An installation names one version. Publishing another changes nothing for a workspace until an administrator upgrades, which is a second consent. - The bundle is validated once, at publish: the manifest parses, every file it declares is present and loadable, and a surface entry with a top-level import is refused with the line named. That failure used to surface months later in a reader's browser. - The host inlines the surface script into the document it generates, so the CSP names no remote script origin at all. `connect-src 'none'` now means it: a surface with no net: scope cannot reach anywhere, including its author. - Install-by-URL is gone rather than kept alongside. Two paths would make "is this code frozen?" depend on how the plugin was installed. - MULTICA_PLUGIN_DIR stays the development channel, but publishes an ordinary immutable version instead of being a second render path; re-publishing an unchanged version lands as `+dev.N`. Hook endpoints and MCP servers remain on the author's own infrastructure. Migration 376 drops existing installations: they name a source URL that is no longer a concept, and the code they would run was never published here. plugins_v1 has never left its feature flag and holds no production data — the same call migration 344 made. Closes MUL-6469 Co-authored-by: multica-agent <[email protected]> * fix(plugins): serialize publish/install/delete, correct the isolation claim Review of #7321 found five problems. Four are fixed here; the fifth is real, needs its own design pass, and is now tracked and described honestly in the code instead of claimed away. Migration prefix collision. main took 376 for agent_task_durable_work_dir while this branch was open, so `migration prefix 376 is reused` failed backend-tests. Rebased and renumbered the six migrations to 377-382. TOCTOU between delete and install/publish. Relationships are application-owned by repository policy, so nothing made "the version this installation names still exists" true across statements: `delete counts zero installs` -> `install reads the version` -> `delete commits` -> `install commits` left an installation whose panel 404s forever, and a publish racing a delete left a version whose package row was gone. Publish, install and delete now hold a (workspace, plugin key) advisory lock for their whole transaction, and delete counts installations inside it. Both interleavings have regression tests that reproduce the bug with the lock removed. Failed publishes moved package state. upsertPackage ran before the version transaction, so a republish that lost the unique index had already renamed the package, and a first publish that failed afterwards left a plugin with zero versions. It now runs inside the same transaction. The top-level import check was wrong in BOTH directions. It read line prefixes, so it missed ` import x` and `/* c */ import x` — and it refused a valid file whose template literal contained the word at a line start, which is ordinary in a surface that renders code samples. A refusal blocks a publish with no way around it, so that false positive was the worse half. Replaced with a scanner that skips comments and string/template literals; where it is imprecise it is imprecise safely (`/` always reads as division), so a mistake can only cost a detection, never invent one. Sandbox self-navigation is NOT fixed. `sandbox="allow-scripts"` permits `_self` navigation by design, no shipped CSP directive covers it, and contentWindow is unchanged across it — so a hostile artifact can still reach its author once and hand the replacement document the bridge port. Closing it needs an embedder frame-src policy or a handshake a navigated document cannot complete; both are decisions of their own. What lands here is damage control, labelled as such: the generated document beacons on pagehide from a listener the plugin cannot detach, and the embedder drops the bridge and unmounts the frame. The "a surface cannot reach its author" wording is removed from the SDK README and the tests. Closes MUL-6469 Refs MUL-6485 Co-authored-by: multica-agent <[email protected]> * feat(plugins): isolate hosted surfaces (MUL-6485) Co-authored-by: multica-agent <[email protected]> * feat(plugins): host plugin artifacts, bind installs to immutable versions A plugin used to be a URL. The manifest was frozen at install, but the surface script was fetched from the author's server every time a panel opened, with no integrity check — so an administrator consented to a manifest while the browser ran whatever that host served that day, inside the scopes already granted. The author's uptime was our uptime, every panel open leaked the reader's IP and "who read which issue" to the author, and publishing at all required a public HTTPS domain, which is why the repo's own deploy-sentinel example never ran. The author now uploads an artifact bundle and Multica stores it: - plugin_package / plugin_package_version / plugin_package_file. A version is insert-only, and the (package_id, version) unique index is what makes immutability a database rule rather than a convention. - An installation names one version. Publishing another changes nothing for a workspace until an administrator upgrades, which is a second consent. - The bundle is validated once, at publish: the manifest parses, every file it declares is present and loadable, and a surface entry with a top-level import is refused with the line named. That failure used to surface months later in a reader's browser. - The host inlines the surface script into the document it generates, so the CSP names no remote script origin at all. `connect-src 'none'` now means it: a surface with no net: scope cannot reach anywhere, including its author. - Install-by-URL is gone rather than kept alongside. Two paths would make "is this code frozen?" depend on how the plugin was installed. - MULTICA_PLUGIN_DIR stays the development channel, but publishes an ordinary immutable version instead of being a second render path; re-publishing an unchanged version lands as `+dev.N`. Hook endpoints and MCP servers remain on the author's own infrastructure. Migration 376 drops existing installations: they name a source URL that is no longer a concept, and the code they would run was never published here. plugins_v1 has never left its feature flag and holds no production data — the same call migration 344 made. Closes MUL-6469 Co-authored-by: multica-agent <[email protected]> * fix(plugins): serialize publish/install/delete, correct the isolation claim Review of #7321 found five problems. Four are fixed here; the fifth is real, needs its own design pass, and is now tracked and described honestly in the code instead of claimed away. Migration prefix collision. main took 376 for agent_task_durable_work_dir while this branch was open, so `migration prefix 376 is reused` failed backend-tests. Rebased and renumbered the six migrations to 377-382. TOCTOU between delete and install/publish. Relationships are application-owned by repository policy, so nothing made "the version this installation names still exists" true across statements: `delete counts zero installs` -> `install reads the version` -> `delete commits` -> `install commits` left an installation whose panel 404s forever, and a publish racing a delete left a version whose package row was gone. Publish, install and delete now hold a (workspace, plugin key) advisory lock for their whole transaction, and delete counts installations inside it. Both interleavings have regression tests that reproduce the bug with the lock removed. Failed publishes moved package state. upsertPackage ran before the version transaction, so a republish that lost the unique index had already renamed the package, and a first publish that failed afterwards left a plugin with zero versions. It now runs inside the same transaction. The top-level import check was wrong in BOTH directions. It read line prefixes, so it missed ` import x` and `/* c */ import x` — and it refused a valid file whose template literal contained the word at a line start, which is ordinary in a surface that renders code samples. A refusal blocks a publish with no way around it, so that false positive was the worse half. Replaced with a scanner that skips comments and string/template literals; where it is imprecise it is imprecise safely (`/` always reads as division), so a mistake can only cost a detection, never invent one. Sandbox self-navigation is NOT fixed. `sandbox="allow-scripts"` permits `_self` navigation by design, no shipped CSP directive covers it, and contentWindow is unchanged across it — so a hostile artifact can still reach its author once and hand the replacement document the bridge port. Closing it needs an embedder frame-src policy or a handshake a navigated document cannot complete; both are decisions of their own. What lands here is damage control, labelled as such: the generated document beacons on pagehide from a listener the plugin cannot detach, and the embedder drops the bridge and unmounts the frame. The "a surface cannot reach its author" wording is removed from the SDK README and the tests. Closes MUL-6469 Refs MUL-6485 Co-authored-by: multica-agent <[email protected]> * fix(plugins): harden surface validation and error reporting * fix(migrations): renumber plugin package migrations * fix(plugins): validate classic surfaces and reset failures * fix(plugins): refresh issue-scoped surface launches Co-authored-by: multica-agent <[email protected]> * feat(plugins): isolate hosted surfaces (MUL-6485) Co-authored-by: multica-agent <[email protected]> * fix(plugins): refresh issue-scoped surface launches Co-authored-by: multica-agent <[email protected]> * fix(plugins): make surface termination event-driven Co-authored-by: multica-agent <[email protected]> * feat(plugins): isolate hosted surfaces (MUL-6485) Co-authored-by: multica-agent <[email protected]> * fix(plugins): refresh issue-scoped surface launches Co-authored-by: multica-agent <[email protected]> * fix(plugins): make surface termination event-driven Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5a80f802d3 |
MUL-6558 feat(server): make task queue expiry configurable via MULTICA_TASK_QUEUED_TTL (#7418)
Lets self-hosted deployments raise the queued-task expiry window above the built-in 2h default, so low-concurrency runtimes stop losing legitimately-pending work to queued_expired. Default behavior is unchanged. Covers .env.example, Compose, Helm values/ConfigMap, and the behavior docs in all four locales. Co-authored-by: Martin Montes <[email protected]> |
||
|
|
53e15b35c6 |
MUL-6502: fix startup recovery during PostgreSQL outages (#7372)
* fix(startup): retry transient database outages Co-authored-by: multica-agent <[email protected]> * fix(startup): address database retry review Co-authored-by: multica-agent <[email protected]> * test(startup): address retry review nits Co-authored-by: multica-agent <[email protected]> * fix(startup): preserve native pgx connect timeout Co-authored-by: multica-agent <[email protected]> * fix(startup): apply timeout fallback to services Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5e2d349ec4 |
MUL-6426: refactor(dingtalk): safely retire group routing and enhance discovery (#7241)
Retire mutable DingTalk group routing: inbound messages always target the installation's configured Agent, and observed groups become read-only inventory in dedicated tables with persisted activity counters. Migrations 382-389 clean stale cross-Agent bindings, create and backfill dingtalk_group_presence and dingtalk_bot_identity, and install a mirror trigger so writes from an older process during a rollback window are not lost. The legacy dingtalk_group_route table and its indexes are retained for expand/contract; a later release drops the trigger and table together. Closes #7239 |
||
|
|
8febf40970 |
MUL-6525: docs: document GitHub App Contents permission for PR snapshots (#7369)
Add Contents: Read-only to the self-hosting GitHub App permission table in all four locales, note that it cannot be skipped along with the CI permissions, and list it in the No CI status troubleshooting entry. Co-authored-by: Sebastian San Blas <[email protected]> |
||
|
|
a3bd38da6a |
fix(cli): redact autopilot webhook credentials (#7365)
* fix(cli): redact autopilot webhook credentials * fix(cli): preserve webhook token presence metadata |
||
|
|
814ee0cbf7 |
docs: remove six finished implementation records (#7258)
* docs: remove the two implementation docs under docs/ The database index audit and the Telegram channel review were one-off implementation records for MUL-6108 and the Telegram contribution; both are finished work and neither is referenced by the product docs. Drops the now-dangling review pointer from the Telegram handoff notes. docs/assets/ stays — the READMEs load the logos from it. Co-authored-by: multica-agent <[email protected]> * docs: remove the Telegram integration handoff notes HANDOFF-telegram.md was the implementation record for the Telegram channel contribution and is referenced by nothing. Operator-facing setup lives in the localized Telegram guides under apps/docs/content/docs/. Co-authored-by: multica-agent <[email protected]> * docs: remove three finished implementation leftovers - apps/mobile/docs/markdown-renderer-research.md: 2026-05 renderer selection research and incident log. Its conclusions already live in markdown-rendering-adr.md and in the MD_FONT/MD_LINE comments; the HIG calibration source is folded into markdown-style.ts so the numbers keep their rationale. - packages/views/locales/glossary.md: a redirect stub. The content moved to the docs-site conventions pages, which now say the file is gone instead of claiming a stub still points at them. - scripts/audit-redundant-indexes.sql: the MUL-6108 one-off audit query, orphaned once its report was deleted — no caller in Makefile, check.sh or CI. All pointers into the deleted files are updated; nothing dangles. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3268d01172 |
fix(daemon): move the in_progress start write into workflow step 3 (MUL-6417 follow-up 2) (#7295)
* fix(daemon): move the in_progress start write into workflow step 3; define ancillary by output (MUL-6417 follow-up 2) Field evidence from MUL-6460: a run that verifiably received the #7283 brief (its workdir CLAUDE.md carries the two-moment rule) walked steps 1→2→3 for over an hour and never issued a single status write. Two compounding causes, both in the prompt: - Placement. The start-moment rule lived in the status block BELOW the numbered steps. At the moment its condition triggers the model is executing the list, and a rule outside the list does not fire — the pre-MUL-6417 opening write was reliable precisely because it was an explicit step. The conditional write now opens step 3 itself, at the read→work boundary. - The research example. "A todo issue you were asked to research stays todo" was written for consult pull-ins, but the MUL-6460 turn's work WAS research toward its own issue's ask — the example pattern-matched the exact case it was never meant to cover. Ancillary is now defined by output (the turn produced none of the issue's own deliverable), and research/design explicitly count as work when they are the ask. Guard tests pin the step-3 placement and the research-counts-as-work clause, and ban the retired example. Skill, source maps, and user docs updated to match. Co-authored-by: multica-agent <[email protected]> * refactor(daemon): state the status rule as continuous, anchored at the moments that fire (MUL-6417) Bohan's review of #7295: 'at the end of your work, write the state it reached' still read as restricting writes to fixed checkpoints. The lead now states the invariant — keep status true at every point in the turn, write whenever the work changes it, mid-turn included (a blocker is recorded when hit) — and the end of the turn becomes a verification ('do not exit with a stale value') instead of the write moment. The anchors stay. A free-floating 'write when necessary' with no attachment points is exactly the shape that produced zero writes in the MUL-6460 incident: permission without a trigger moment does not fire. Step 3 keeps the start write; the exit check keeps the guarantee that a turn never ends with a lying board. Co-authored-by: multica-agent <[email protected]> * fix(daemon): drop activity-word skip-lists; ancillary is decided by output alone (MUL-6417) Elon's #7295 review: 'review' still sat in step 3's skip clause and the no-write bullet's example list, replaying the research incident's exact failure class on an issue whose ask IS reviewing a PR. Activity words carry no deliverable information in either direction, so both lists are gone: step 3 names forms of work (code, research, a design, a plan, or a review when reviewing is the ask), the no-write case is stated purely by output (the turn produced none of THIS issue's deliverable), and the rule now says so explicitly — the kind of activity never decides, only whether the output is part of what the issue asks for. Guards pin the review-as-work clause and the never-decides sentence, and ban both retired skip-lists. Skill, source map, and docs synced. Measured brief delta vs main (claude, production-length UUID): ordinary 13,362 → 13,971; leader 14,467 → 15,076 (+609 each, cached prefix); per-turn messages unchanged. Co-authored-by: multica-agent <[email protected]> * refactor(daemon): adopt J's review — one criterion, anchors in steps, category-scoped skip (MUL-6417) J's review of #7295, all four points taken: - The positive form-list that replaced the banned skip-list was the same failure shape read as exhaustive (a triage or reproduce-the-bug turn finds itself in no list). Both directions of activity-word lists are gone; the never-decides sentence moves INTO step 3 — the position that fires — instead of the status block, the position this PR itself proved dead. - The exit-side check is anchored inside step 5 ('Before exiting, ...'), making the PR's own placement argument self-consistent. - The step-3 skip is scoped to the in_progress CATEGORY, so a custom status like Planning already counts as recorded once MUL-6460 puts the status catalog in front of agents — no rewording needed then. - Tense: 'if any part of what this turn will produce ...' — step 3 is a prediction; mispredictions self-heal under the continuous rule. Plus J's meta-point on guards: presence-pins cannot tell where a sentence lives, and both field incidents were correct sentences in dead positions — the anchors are now pinned positionally (must appear inside the step-3 / step-5 lines), and the retired positive list joins the banned set. Kept the questions/discussion/acknowledgements clause (J's conservative option): acknowledgements are not covered by 'answered a question' and the sentence is the MUL-6300-era invariant other tests pin. Measured (claude, production-length UUID): ordinary brief 13,685, leader 14,790 — +243 vs main, down from +609 at the previous commit. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
cc21d19353 |
MUL-6463: confirm the promotion that starts an agent run (#7293)
* feat(issues): confirm the promotion that starts an agent run (MUL-6463) Assigning an agent to an issue opens a confirmation with a handoff note and "Don't start yet". Promoting a parked issue that ALREADY has an agent owner starts a run just as directly — the status change alone is what fires RunSourceStatus — but applied silently from a one-click picker, with no way to tell the agent anything before it woke up. The dialog now covers both writes. `RunConfirmModal` gains a `promote` mode that submits the status change (plus the same optional handoff note, or suppress_run for "Don't start yet") instead of an assignee change; the owner is already on the issue, so the promotion payload carries the status and nothing else. The gate is the run predicate, not a status allow-list: parked (backlog CATEGORY) + agent/squad owner + a target outside backlog/done/cancelled. So built-in `todo` and every custom Todo-category status behave identically, which is what the report asked for, and so do the other promotion targets that start a run the same way. Drag-and-drop and the batch toolbar keep applying directly — the existing split (MUL-4155), not a new one. Also fixes the assign gate next to it, which still compared the raw status KEY to "backlog": an issue parked in a CUSTOM backlog-category status popped a confirm promising a run the server would never start. Both gates now resolve the category the way the server does (issuestatus.Effective), preferring the payload's own status_category and falling back to the workspace catalog. Tests: promotion routing for built-in todo / custom todo / custom backlog origin / non-todo target, the five no-run status changes that stay direct, the custom-backlog assign regression, and the modal's promote payloads (note, suppress_run, no assignee fields) plus its status naming. Verified each fails without the change. Docs: the promotion dialog in assigning-issues (en/zh/ja/ko). Co-authored-by: multica-agent <[email protected]> * fix(issues): bold the status name in the promotion confirm "Move this issue to Rework." left the status looking like an ordinary word — worse for a custom status an admin named something like Later or Blocked-ish. The headline already had a fence-and-bold mechanism for the assignee name, so generalize it to bold every fenced span and fence the status too. CJK copy drops the 「」 it used for the same job: bold is now the one mechanism across all four locales. Co-authored-by: multica-agent <[email protected]> * fix(issues): close the three run-confirm gaps from review (MUL-6463) 1. Server and client disagreed about moving WITHIN the backlog category. `WillEnqueueRun`'s status branch only excluded `done`/`cancelled`, so `backlog` → a custom Backlog-category status (`Later`) changed the key, normalized both sides to `backlog`, and still enqueued a run — while the client, which reads categories, applied it silently. Before custom statuses the two conditions were the same; now leaving the category is its own requirement. Regression test covers both halves: no run inside the category, still exactly one on the way out. 2. The table's inline pickers bypassed the gate entirely — `meta.updateIssue` went straight to the surface action, so promoting an agent-owned backlog row from the table started a run with no confirmation. `meta.updateIssue` now takes the issue (the gate needs its category and owner) and routes through the same intent. 3. "Unresolvable category" was collapsing into a guess. `catalog.categoryOf` answers `todo` for a custom key it has not loaded, which made an unknown status read as not-parked and skipped the promote dialog. Category resolution is now explicitly three-state — carried, built-in, catalog, else null — and both writes that can start a run confirm on null. The gate itself moves to a pure `runConfirmIntent` next to a canonical matrix test, so the issue detail, the context menu and the table all route on one answer instead of three copies; the hook/table suites keep only their wiring. Docs nit from the same review: the new CJK paragraphs said issue / イシュー / 이슈 where the glossary requires 任务 / タスク / 태스크, and named statuses in capitalized prose instead of backticked keys. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
a1d160ee0d |
MUL-6468: fix(docs): replace dead dimcode.ai link with official Dim CLI docs (#7294)
* fix(docs): replace dead dimcode.ai link with dimagent.cn/docs/cli The install-agent-runtime guide linked Dim to https://dimcode.ai which does not resolve. Replace with the official CLI docs at https://dimagent.cn/docs/cli across all 4 language versions (en/zh/ja/ko). * fix(docs): use English Dim CLI links for non-Chinese docs Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
49ed7f1151 |
MUL-6459: docs: document custom issue statuses (#7284)
* docs: document custom issue statuses (MUL-6459) The docs still described the seven statuses as a fixed set. Add a Custom statuses section to the Issues page in all four locales covering the model users actually need: the seven categories are fixed and each one IS its built-in status key, a custom status declares a category at creation and inherits its platform behavior in full, and that category is immutable afterwards. Also state the consequences that are easy to get wrong: board columns are categories so a new status never adds a column, built-in statuses are locked, and archiving retires a status for future assignment without touching the issues already on it. Two rules elsewhere were written as status rules but are category rules: the backlog gate on assignment and the auto-archive of run-failure inbox notifications. Both now say so. Co-authored-by: multica-agent <[email protected]> * docs: correct the run-start trigger, category scope, and key wording Review follow-up on the custom statuses section (MUL-6459). The run-start trigger was attributed to entering `todo`. The only status-change trigger is leaving the `backlog` category for anything other than `done` / `cancelled` (service/issue_trigger.go), so the rule now sits on the `backlog` row where it belongs. The claim that every status named in the docs means its category was too broad, and contradicted the system-changes callout on the same page: the failed-run rollback writes the literal `todo` key (service/task.go) and a merged PR writes the literal `done` key (handler/github.go). Scope it to statuses that TRIGGER behavior, and say the write direction is literal. Key derivation was described as always coming from the name. The API takes an explicit key and only derives one when it is omitted (handler/issue_status.go), and the key never changes on rename. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4ff37edf91 |
fix(daemon): record in_progress when work on the issue's ask starts, not only at turn end (MUL-6417 follow-up) (#7283)
Post-merge report from Bohan: a todo issue assigned to an agent sat in todo for the whole first work turn — half an hour of active work the board attributed to nobody. The end-of-turn-only rule conflated when the agent can JUDGE with when the FACT changes: a turn that advances the issue's own ask makes "being worked" true the moment it begins. The activity indicator shows the run, but columns, filters, and sorting read status — the indicator alone is not the surface people watch. The status rule now has two write moments, same fact anchor: - start: the moment the turn is known to advance the issue's own ask → in_progress (only if it differs — rework on an in_review issue flips it back the same way); - end: the state the work reached (in_review / in_progress-continuing / blocked). An ancillary turn (research, answers, review, discussion) still writes nothing at either moment, so the research-stays-todo case and the concurrent-convergence property from the original MUL-6417 change are unchanged. This is the pre-MUL-6417 timing with the fact anchor's conditionality — not a revert to the unconditional lifecycle flip. Docs, skill reference, and source maps updated to the two-moment rule; guard tests pin the start moment and ban the end-only phrasing. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
e1be1625cb |
MUL-5959: feat(agent): add Dim (DimCode) ACP runtime (#6675)
* feat(agent): add Dim (DimCode) ACP runtime Add Dim (dimcode, the `dim` CLI) as a first-party agent runtime, driven over the ACP (Agent Client Protocol) transport via `dim acp`. Key integration points: - New dim backend (pkg/agent/dim.go): spawns `dim acp`, performs the ACP initialize/session/new handshake, and raises the runtime's hardcoded read-only permission preset to full-access (plus agent mode) via session/set_config_option before the first prompt — without it every file write is denied by a capability rule. Model override uses session/set_model; the model catalog is read from session/new. - Session resume is intentionally skipped: Dim binds sessions to the creating process, so a later process's session/load is rejected with "held by another process" even after a clean session/close. The backend always starts a fresh session and reports ResumeRejected so the daemon classifies the run correctly. A best-effort session/close keeps Dim's own session list free of orphaned entries. - Registration: SupportedTypes whitelist, New() factory, launch header, daemon probe (MULTICA_DIM_PATH / MULTICA_DIM_MODEL), default agent command name, protocol_family CHECK migration (255), model discovery, MCP config support, metrics label, and runtime display. - Docs/UI: provider logo, landing i18n (en/zh/ja/ko), providers and install-agent-runtime docs, README, CLI_AND_DAEMON, SELF_HOSTING. - Tests: dim backend unit tests (fresh session + resume behavior), shared ACP deliverable case, logo and MCP-support tests. * feat(views): use official DimCode app icon for Dim provider logo Replace the placeholder inline SVG with the official DimCode desktop client app icon (from /opt/DimAgent/resources/build/icon.png, resized to 128px), imported as a static asset the same way the Qwen Code mark is handled. * test(agent): add real dim ACP smoke tests (agentintegration) Two gated tests behind MULTICA_RUN_REAL_AGENT_SMOKE=1: - TestDimRealACPSmoke: full end-to-end against real `dim acp` — initialize, session/new, set_config_option (permission/mode), prompt, and assert completed output. - TestDimRealResumeRejected: pass a fake ResumeSessionID, assert the backend starts a fresh session (no session/load), and reports ResumeRejected=true so the daemon classifies the run correctly. Verified against dimcode 0.3.2 and 0.3.8. * fix: renumber dim migration 265 → 271 (upstream added 265-270) * fix: renumber dim migration to 272 (upstream added 271) * fix(agent): dim cross-run resume via session/load + review fixes Address review feedback on the Dim ACP runtime: - #1 Deliver runtime brief to Dim: add dim to the AGENTS.md provider list (verified dim 0.3.8+ reads AGENTS.md from the session cwd). - #2 Cross-run session continuity: dim 0.3.10+ releases its per-process session lock ~5s after the owning process exits, so resume now goes through the standard ACP session/load (same as traecli/kiro/grok) instead of always starting fresh. A loaded session retains its permission/mode, so set_config_option runs only on fresh sessions. Add a two-run regression (TestDimRealCrossRunResume) proving run B recalls context established only in run A. - #3 Process lifecycle races: the deferred cleanup now cancels the run context before cmd.Wait (a child ignoring stdin EOF no longer hangs Result delivery) with a bounded force-kill fallback; the success path waits for the final prompt notification with a grace window instead of a non-blocking read that could miss the last message. - #4 Isolate model discovery by executable path (discoveryCacheKey). - #6 Real smoke test now writes a sentinel file to prove full-access is effective; a best-effort session/close is sent before teardown. - #7 gofmt. * test(agent): add dim process-lifecycle regressions (review #3) - TestDimCleanupKillsHangingChild: a child that ignores stdin EOF and SIGTERM after an early failure is force-killed within dimProcessWaitTimeout, so Result still closes. - TestDimPromptMissingNotificationStillCompletes: when session/prompt returns without stopReason (onPromptDone never fires), the bounded final-notification wait falls through and Result completes instead of hanging. * fix(migrations): renumber dim runtime_profile migration 272 → 273 (upstream added 272) * fix(agent): review round 2 — process tree, quiescence, fail-closed, version check Address all remaining blockers from the second review: - #1 Require dim >= 0.3.10 (version check at initialize); bounded retry on session/load when the lock is not yet released instead of silently starting fresh; integration test no longer sleeps before resume. - #2 Process-tree cleanup: configureProcessGroup + signalProcessGroup + waitProcessGroupGone; cmd.Cancel returns nil so we own all signalling; regression asserts the descendant PID is gone. - #3 Notification quiescence: onActivity + waitForACPNotificationQuiescence (same as hermes); Dim equivalent of the late-final-notification test. - #4 Permission fail-closed: set_config_option runs on BOTH fresh and resumed sessions; on config failure, session/close is sent before returning so a partially configured session is not resumed; regression. - #5 Migration renumbered 273 → 274 (upstream added 273). - #6 AGENTS.md mapping regression, two-executable cache isolation test, session/close unit assertion, smoke test now executes a command. - #7 README.md + CLI_AND_DAEMON.md updated with 22 CLIs including Dim. * chore: remove temporary review actions doc (not for PR) * fix(migrations): renumber dim migration 274 → 310 (upstream added 274-309) * review: fix retry break scope, dedup isACPSessionNotFound comment, update stale notes * fix(agent): pass *exec.Cmd to signalProcessGroup/waitProcessGroupGone (upstream signature change) * fix(agent): close session on set_model failure + resume regression test Audit-found gaps from self-review: - set_model failure now sends session/close before returning (same as set_config_option failure) — reviewer #4 said 'permission, mode, or model'. - Add TestDimConfigFailThenResumeReestablishes: run A config fails → session/close sent → run B resumes and re-applies set_config_option (fail-closed), completing successfully. - Fix two stale comments that contradicted the code (config block now runs on both fresh and resumed sessions). * fix(agent): add WaitDelay, use labeled break in retry loop Self-audit improvements: - Add cmd.WaitDelay=10s for consistency with claude.go (hard backstop if a process somehow survives SIGKILL). - Use labeled break (break loadRetry) so runCtx cancellation during the retry delay exits the for loop directly, not just the select. - Update PR description: migration 273→310, set_config_option now re-applied on both fresh and resumed sessions. * fix(migrations): renumber dim migration 310 → 313 (upstream added 310-312) * fix(migrations): remove stale 313 dim migration (replaced by 314 after upstream added dsh at 313) * fix: DSH compatibility + thinking levels + reviewer round 3 Systematic fix of all 39 items from the compatibility gap analysis: Rebase + migration: - Rebase to latest upstream/main (resolves all DSH conflicts) - Migration 314_runtime_profile_add_dim (whitelist includes both dsh + dim) - Remove stale 313 dim migration DSH + Dim coexistence (provider lists, counts, docs): - SupportedTypes, config.go, metrics labels: both dsh + dim - i18n (4 files): count 23, lists include DSH + Dim + Oh-My-Pi - README.md/README.zh.md: count 23 - CLI_AND_DAEMON.md: 23-row table - environment-variables.mdx (4 langs): MULTICA_DIM_PATH/MODEL callout - display.ts, mcp-support.ts, types/agent.ts, provider-logo.tsx: both dsh+dim Thinking levels (was MISSING — dim supports thought_level): - dim.go: call applyACPEffortOption after set_model - thinking.go: add dim to acpCatalogThinkingProviders - dim.go: retain sessionResult for effort option Reviewer round 3: - Version check fail-closed: empty/malformed → reject (not allow) - Cache test through ListModels call site with fake executables - set_model failure regression test - Test companions: sidecar_manifest_test, runtime_config_test add dim Code quality: - agent.go: fix unreachable duplicate return (rebase artifact) - dim.go: labeled break in retry loop, WaitDelay, session/close on set_model fail * fix: deliverable test fake version + provider-logo rebase artifact * fix(migrations): renumber dim migration 314 → 315 (upstream added 314_workspace_mcp_config) * fix(migrations): renumber dim migration 315 → 319 (upstream added 315-318) * fix: Windows process tree, MinVersions, retry tests, migration 327 Address all 5 remaining blockers from review round 4: 1. Windows process-tree ownership: use startOwnedProcessTree instead of cmd.Start(), releaseProcessGroup in cleanup defer. This attaches the child to a Job Object on Windows (no-op on Unix), ensuring descendants are captured and terminated. 2. Migration renumbered 319 → 327 (upstream added 319-326). 3. Add dim: 0.3.10 to MinVersions in version.go so the daemon registers old Dim binaries as offline and refuses triggers (defense in depth on top of the ACP agentInfo.version check in dim.go). 4. Session-lock retry contract tests: success after bounded retries, exhaustion without falling through to session/new. Fake script supports DIM_LOAD_HELD_N (held for N calls then succeed) and DIM_LOAD_HELD_ALWAYS (always held). 5. README.md:206 20→23 runtimes. PR description updated. * test: add missing reviewer-requested regressions (round 4) - Windows Job Object regression: dim_windows_test.go proves the Dim backend captures and terminates descendants via startOwnedProcessTree (Windows-only build tag; companion to TestStartOwnedProcessTree). - MinVersions registration: add dim test cases to TestCheckMinVersion (0.3.10 ok, 0.3.9 rejected, invalid rejected). - Retry cancellation: TestDimSessionLoadRetryCancelled cancels the context during the retry delay, asserts no session/new fallback. * fix: sync upstream Command signature changes + restore dim additions Upstream changed ListModels/discoverXxxModels/detectCLIVersion to accept Command (struct with Path+Prefix) instead of string. The previous rebase kept our old-signature versions, causing widespread compile failures. Restored all affected files from upstream, then re-applied dim-specific additions: - agent.go: dim in SupportedTypes, New(), launchHeaders - hermes.go: isACPHeldByProcess + -32002 in isACPSessionNotFound - thinking.go: dim in acpCatalogThinkingProviders - models.go: dim case in ListModels + discoverDimModels - version.go: dim in MinVersions - All dim test files preserved (dim_test.go, dim_integration_test.go, dim_windows_test.go, models_test.go dim cache test) * fix: rebase to latest main, migration 327→341, resolve DetectVersion conflict * fix: remove unused os/exec import in dim_windows_test.go * fix(migrations): renumber dim migration 341 → 342 (upstream added 341) * fix: use Command.exec instead of exec.CommandContext (upstream GH #7046) Upstream's TestOnlyLaunchGoSpawnsRuntimeProcesses requires all backends to build processes through Command.exec (launch.go), so a custom runtime's fixed_args are carried into the subprocess. Dim was the only backend still using exec.CommandContext directly. * fix: launch prefix policy + remove empty test files + deterministic retry tests Address all 4 findings from review round 5: 1. Add "dim": dimBlockedArgs to launchPrefixBlockedArgs so protocol-breaking flags (--help/--auth-setup/--remote) are filtered from fixed_args. 2. Remove 77 zero-byte *_test.go files accidentally added at repo root. 3. Make retry tests deterministic: TestDimSessionLoadRetryCancelled waits for the first session/load request before cancelling (no fixed sleep); TestDimSessionLoadRetryExhausted asserts exactly 4 load attempts. 4. PR description will be updated separately. * fix: rebase to latest main, migration 342→343 (upstream added mcode at 342) * chore: trigger CI * chore: refresh PR * fix: restore mcode that was lost during merge conflict resolution Merge took 'ours' side which predated mcode. Restore mcode in: - SupportedTypes, New() factory, launchHeaders - agent_supported_types_test.go want map - metrics/labels.go - config.go defaultAgentCommandNames - migration 343 up/down whitelists * fix: restore mcode probe in agents_probe.go + agent-cli-command-names.txt * fix: restore all remaining mcode references lost during merge Files fixed: - README.md: mcode row in runtimes table - mcp-support.test.ts: mcode assertion - config.go: mcode in Agents comment + error message - runtime_config.go: mcode in AGENTS.md case - version.go: mcode in MinVersions - version_test.go: mcode test cases Verified: no file has fewer mcode references than upstream. * fix: restore mcode in environment-variables docs + CLI_AND_DAEMON ACP list - environment-variables.{mdx,ja,ko,zh}: restored 'MiniMax Code' in the QwenPaw model-variable sentence - CLI_AND_DAEMON.md: added MiniMax Code to ACP-family list + loadSession fallback description Verified: every changed file now has >= mcode references vs upstream. * fix(migrations): renumber dim migration 343 → 344 (upstream added 343) * fix(migrations): renumber dim migration 344 → 348 (upstream added 344-347 plugin migrations) * fix(migrations): update down migration comment 344 → 348 * chore: retrigger CI (flaky TestOpenclawDiscoveryCacheConcurrentPreparations — upstream test, passes locally x5) * fix(migrations): renumber dim migration 348 → 352 (upstream added 348-351) * fix: Dim launch-prefix regression test + version.go comment repair (review #6) - Add TestDimLaunchPrefixFiltersBlockedFlags: proves allowed prefix reaches command before acp, and --help/--auth-setup/--remote/-h are stripped from Dim's launch prefix. - Add dim to TestLaunchPrefixReachesACPFamilies family list. - Repair version.go: dim's cross-run session/load comment was appended to mcode entry during a rebase conflict; restore two independent accurate comments. * fix(migrations): update down migration comment 348 → 352 * chore: retrigger CI (backend-tests stuck 50+ min) * fix(migrations): renumber dim migration 352 → 362 (upstream added 352-361) * fix: use logAgentCommand for dim argv logging (upstream redaction requirement) Upstream's TestOnlyLaunchGoLogsAgentCommandArgs enforces that all runtime argv logging goes through Config.logAgentCommand so sensitive args are redacted. dim.go was still using Logger.Info directly. * fix(migrations): renumber dim migration to 370 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
658b0b7d9f |
MUL-6406: fix(auth): fail fast in production on insecure JWT secret defaults (#7212)
* fix(auth): fail fast in production on insecure JWT secret defaults * chore(deploy): require a strong JWT_SECRET in compose and docs * fix(ci): make selfhost config test pass with required JWT_SECRET The compose file's JWT_SECRET:? error message contained a colon-space sequence that YAML parsed as a mapping indicator, breaking docker compose config. Quote the openssl hint instead. .env.example now ships an empty JWT_SECRET, so the test's docker compose config calls fail interpolation; supply a throwaway secret from the environment, which Compose lets outrank the env file. * fix(ci): seed JWT_SECRET into the recipe .env in selfhost config test The Makefile includes .env and bare-exports every variable to the recipe environment, so the make-driven recipe cases clobbered the test's JWT_SECRET export with the empty value .env.example ships. The stub's `docker compose config` then failed interpolation with stderr discarded, produced empty JSON, and crashed the node parser. Seed the throwaway secret into the recipe .env, which both make and Compose load. * docs(env): use a CSPRNG for the Windows JWT_SECRET generation hint Get-Random is not cryptographically secure, so the Windows hint could leave self-hosters with a predictable signing key. Point at RandomNumberGenerator instead, per review. * docs(selfhost): fix stale JWT secret comments Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4f56785b02 |
refactor(daemon): merge Reply/Ownership turn modes into one fact-driven workflow (MUL-6417) (#7245)
* refactor(daemon): merge Reply/Ownership turn modes into one fact-driven workflow (MUL-6417)
The Reply/Ownership split dated from PR #205 and had already been merged
in substance by MUL-5377 (one brief section + mode router) and MUL-6300
(reply turns own the same status arc). What remained was a per-turn
marker plus two mode blocks restating information the per-turn message
already carries as data. This finishes the merge:
- Status is judged once, at the END of the turn, from what the work
actually changed about the issue — not from the trigger type, not from
the run lifecycle, and not gated on being the assignee. No opening
in_progress write: the board's in-flight signal is the issue activity
indicator, and a todo issue the agent was only asked to research now
correctly stays todo (the old unconditional arc got that wrong twice).
Lifecycle writes oscillate under concurrent runs; fact writes converge,
which is what the retired assignee gate existed to protect.
- Delivery routes on data: a triggering comment in the per-turn message
(reply in that thread with THIS turn's --parent, one reply per thread)
or none (post a new top-level comment).
- The blocked rule and the squad-leader dispatch case become end states
of the same judgment instead of mode-block special cases; the leader
keeps one bullet (dispatching members is not delivery). The reply-mode
mention pointer is gone — the discipline lives in ## Mentions.
- The Turn mode markers are removed from the per-turn prompts, deleting
the marker/router-drift failure class (
|
||
|
|
f7abcdded3 |
docs: publish the community-maintained areas and their maintainers (#7249)
Until now, "community-maintained" appeared in the docs without ever naming who maintains what, and the actual maintainers were recorded only in Go package comments and PR threads. Users filing an issue had no way to learn any of it. Add a Community-maintained areas page in all four locales listing each area, its code path, its maintainers, and the retirement rule, and make that page the single source of truth: channels, the DingTalk and Telegram guides, the README, and the three package doc comments now point at it instead of restating it. Those package comments no longer carry maintainer handles or a second copy of the policy, so ownership changes are recorded in exactly one place. The page is deliberately a credit-and-expectations page, not a support directory — it asks readers to file a GitHub issue rather than page a volunteer directly, since what we promised these maintainers is that we would route work to them, not that they are on the hook for it. It also avoids the term "code owner": we are not adding a GitHub CODEOWNERS file, and that term carries a specific automated meaning there. Both issue forms gain a required Area field so a report can be routed without asking the reporter to read source, with the community-maintained areas marked as such in the option list. Co-authored-by: multica-agent <[email protected]> |
||
|
|
67c6aa05c4 |
MUL-6166: feat(telegram): add Telegram channel integration (#6944)
* feat(telegram): add Telegram channel integration Co-authored-by: multica-agent <[email protected]> * chore(telegram): rebase onto main and renumber migrations to 366-367 349/350 collided with main's agent_task_queue_chat_terminal_resume_index and agent_task_queue_chat_retired_session_index. Renumber the Telegram origin pair to the next free prefixes, update the cross-references inside the SQL comments, and point telegram_origin_migration_test.go at the new filenames. Contributor-facing behavior is unchanged. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> Co-authored-by: Bohan-J <[email protected]> |
||
|
|
801c671973 |
chore(assets): convert bitmaps to WebP, dedupe, and gate image size (MUL-6352) (#7149)
* chore(assets): convert web and docs bitmaps to WebP The repo carried 21.7MB of PNG/JPG and zero modern formats. next/image already serves visitors AVIF/WebP, but the raw masters are still paid for on every clone, every Docker build context, and every deploy upload. Re-encode with cwebp and cap the source width at the largest size any layout can request: 1920px for the decorative landing backgrounds (they render behind content under object-cover), 2640px for the landing hero (2x its 1320px slot), 1600px for screenshots (2x the ~720px docs and use-case column). EXIF goes with it — the four feature backgrounds were 144dpi print exports. Encoder choice is per image rather than uniform. Most compress best as lossy; two of the docs screenshots are flat palettized PNGs that encode smaller with `-lossless -z 9`, so they use that instead. Screenshots are q80 to keep UI text crisp, decorative art q72. apps/web/public 10.70MB -> 1.57MB apps/docs/public 7.47MB -> 2.44MB Verified: 1:1 crops against losslessly-downscaled references show no visible difference; all 266 image references in the tree resolve; docs prerenders all 177 pages and the landing/use-case pages render with no broken images. Co-authored-by: multica-agent <[email protected]> * chore(assets): drop the duplicated README hero image docs/assets/hero-board.png was byte-for-byte identical to the docs site's workspace-overview screenshot — 633KB stored twice. Only the two READMEs referenced the copy, so point them at the canonical file instead. Co-authored-by: multica-agent <[email protected]> * chore(assets): losslessly recompress app icons oxipng -o max --strip safe over the app icon PNGs: -640KB with pixels untouched (verified by comparing lossless-WebP encodes of the before and after of every file — all 16 identical). Deliberately not switched to WebP and not regenerated: the prerendered sizes under apps/desktop/build/icons/ exist because electron-builder's auto-derivation silently shipped only the 1024x1024 source in the v0.2.31 .deb (#2515), and the PWA manifest and .icns/.ico containers want PNG. Filenames, dimensions, and the electron-builder config are unchanged, so packaging behaves exactly as before. Co-authored-by: multica-agent <[email protected]> * ci: gate pull requests on a 300KB image budget Keeps the previous three commits from being undone one screenshot at a time. A bitmap added — or grown — past 300KB fails the check until the PR description carries an `Oversized image exemption:` line saying why. Shrinking an oversized file always passes: the check compares against the size at the base ref, not just the absolute size. Path-filtered to PRs that touch a bitmap, so the full-history checkout it needs to diff against the base is only paid for when it is relevant. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3c3e77b00c |
MUL-6362: redact secrets from provider command logs (#7206)
* fix(agent): redact provider command values Co-authored-by: multica-agent <[email protected]> * fix(agent): preserve safe command diagnostics Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
24d8c521a6 |
MUL-6364: make the LLM client retry budget explicit, wired, and validated (#7201)
* feat(llm): make the LLM client retry budget explicit, wired, and validated Config.MaxRetries was a bare int that nothing outside tests ever set. The production wiring in handler.New never passed it and no MULTICA_LLM_* env var fed it, so the field was configurable in name only and every deployment ran on the SDK default. Its semantics were also inverted. Because the zero value and an explicit 0 are the same int, "0" fell into the unset branch and silently produced 2 retries, while a negative — clamped to 0 to dodge the panic in option.WithMaxRetries — was the only value that actually disabled retries. - MaxRetries becomes *int so unset, 0 (disabled) and N (exact) stay distinct. New always passes the budget to the SDK explicitly, including the default, so the enforced policy and the reported one cannot drift apart. - Add MULTICA_LLM_MAX_RETRIES as the single configuration source. Non-numeric, negative and above-ceiling values fail the boot instead of being corrected into something that looks configured. The ceiling of 5 is a latency budget: backoff reaches ~21s at 6 retries while the internal callers of this layer time out after 8s and 20s. - Report the effective policy at startup via Client.RetryBudget, whose fields are counts and a fixed enum so the line cannot carry a key or gateway URL. - Document the four states in .env.example and the environment-variable docs, and record what the budget does not cover: the GenerateJSON parameter negotiation and a stream that breaks after ChatStream returns. Tests pin every state by upstream request count, so the unset-versus-zero collapse cannot come back unnoticed. Refs MUL-6364, closes #7154 Co-authored-by: multica-agent <[email protected]> * fix(llm): reject invalid retry budgets at the client boundary and wire self-host Review found the retry budget still reachable in two invalid shapes, and never reachable at all on the main self-hosted deployment path. - docker-compose.selfhost.yml declares the backend environment as an explicit allowlist and mapped none of the MULTICA_LLM_* variables, so every value .env.example and the docs told a self-hoster to set was dropped before the container: the LLM layer read as unconfigured no matter what they wrote. Map all four, and assert propagation in scripts/selfhost-config.test.sh, plus a drift guard so a future MULTICA_LLM_* documented without a mapping fails the script instead of silently going nowhere. - Config.MaxRetries took a *int, so a negative still reached llm.New, which warned and used the default instead. Warning is not silence, but substituting a different budget is still the implicit correction #7154 asked to remove. It now takes *RetryOverride, whose field is unexported and whose only constructor rejects negatives — the invalid state is unrepresentable, so New has no correction branch left to take. - The budget is a ceiling, not a quota: only retryable failures consume it, and a success or the caller's deadline can end a call sooner. Say "at most N" rather than "exactly N" in the field docs, .env.example and all four environment-variable docs. The negative-value test now asserts rejection instead of pinning the fallback as contract. Refs MUL-6364, closes #7154 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
bb8a04f3a1 |
docs(llm): disclose what the server-side LLM layer sends, and test the zero-request opt-out (MUL-6369) (#7199)
* docs(llm): spell out what the server-side LLM layer sends upstream (MUL-6369) The two MULTICA_LLM_* variables are the only switch governing whether chat content reaches an external model, but nothing said so: the .env.example comment named chat titles as an example consumer, the docs page said only that callers 'use local fallback logic', and neither said what is sent. An operator whose policy forbids chat content leaving the deployment had no way to confirm that leaving the variables empty is sufficient, which is what GitHub issue #7162 reports. Document the layer where it is configured: both consumers (chat auto-titling, follow-up questions) and exactly what each sends, plus the fact that leaving the key and base URL empty is a supported configuration that costs nothing but the LLM-written title and the suggestion buttons. The package doc carries the same inventory next to the code, so a new consumer is added in the file that documents them. Co-authored-by: multica-agent <[email protected]> * test(llm): pin the zero-upstream-request and single-entry-point contracts (MUL-6369) The documentation added in the previous commit makes two promises that were true only by accident. An unconfigured client returning ErrNotConfigured was never asserted to do so without dialing, and 'every server-side LLM call goes through pkg/llm' held only because no one had imported the SDK elsewhere yet. TestUnconfiguredClientMakesZeroUpstreamRequests drives every exported call path through a transport that counts and refuses, then proves the counter can move for a configured client so the zero-count assertions cannot pass vacuously if the HTTPClient seam is ever dropped. TestOutboundLLMTrafficGoesThroughThisPackage parses the server module and fails on an OpenAI SDK import outside this package, because the consumer inventory is only auditable while this package is the only door. Co-authored-by: multica-agent <[email protected]> * fix(llm): scope the privacy claim, and guard the consumer inventory (MUL-6369) Addresses both must-fixes from review. They land together because they share the same surfaces: the package doc is the inventory the operator copy is written from, and it names the tests that hold each half. 1. The disclosure claimed more than the code guarantees. The copy said these variables are the deployment's only switch for sending chat content to a model, and that emptying them means the server sends nothing to any provider. What is actually guaranteed is narrower: server/pkg/llm makes no request. Running an agent reaches a model by an entirely separate path — the daemon executes an AI coding tool under that tool's own credentials, with MULTICA_* stripped from its environment. For a privacy document that gap matters more than it would anywhere else: an admin who reads the old wording concludes emptying two variables stops all model traffic, which is the kind of wrong belief #7162 is about. Every surface now says "this layer sends nothing" and names the agent path explicitly — .env.example, the four docs locales, the package doc, the test comments. 2. The inventory had no guard. The import test proved the OpenAI SDK has one door; it said nothing about how many callers walk through it, so a third feature calling GenerateText would ship undisclosed with every test green while the docs kept naming two consumers. TestDocumentedConsumersAreTheOnlyCallers scans production call sites and requires the set to match the published list in both directions: an undocumented caller fails, and so does a documented one that stopped calling, since an inventory that overstates what is sent misleads too. The import test is renamed TestOpenAISDKIsImportedOnlyByThisPackage to claim only what it checks. Also drops the package doc's stale "drafting a quick-create issue" example, which named a consumer that does not exist and contradicted the inventory two paragraphs below it. Co-authored-by: multica-agent <[email protected]> * fix(llm): correct the MULTICA_* claim about agent environments (MUL-6369) Review caught a factual overreach in the previous fix. The copy said MULTICA_* variables are dropped from the agent's environment rather than passed to it, which is not what mergeEnv does: it drops the namespace the daemon INHERITED, then appends the task-scoped values daemon.go assembled — MULTICA_SERVER_URL and MULTICA_TOKEN reach the child by design, and claude_test.go asserts it. The guarantee an operator actually needs is narrower and still sufficient: the daemon does not forward these MULTICA_LLM_* settings to the AI coding tool, and agent runs use that tool's own credentials. Say that instead, and name the task-scoped injection so the sentence does not contradict the subprocess env contract. Applied to .env.example, the four docs locales and the package doc. Also splits methodNameCollisions out of documentedConsumers, per the same review's non-blocking note. A method-name collision quieted by adding a file to documentedConsumers would have corrupted a list operators read as 'what sends my chat content somewhere' in order to silence a test — a bad trade the guard should not invite. Empty today; the repository has no collisions. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
22db0b0a2c |
MUL-5651: preserve tasks through runtime network partitions (#7129)
* fix(runtime): preserve tasks through network partitions Co-authored-by: multica-agent <[email protected]> * fix(runtime): bound reconnect retry waiting Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
f78c867168 |
docs(tasks): describe the execution log where it actually lives (#7120)
The 'Viewing run history' section read as a two-step navigation ('go to the
Execution log' then 'open a run record'), which does not match the product:
ExecutionLogSection renders inline in the issue's right sidebar, expanded by
default, with a one-click View transcript on each row.
An external report (#7107) asked for an inline collapsible execution view on
the issue page based on this wording, and separately asked whether issue
pages auto-refresh. Both already exist; the docs described neither.
- Say the log lives in the issue's right sidebar, not a separate page/tab
- Note the default-expanded state, pinned active runs, Show past runs
- Point at the sidebar panel button for when the section is not visible
- State that the log updates live over the realtime connection
- Mention the header live chip and the transcript's Newest first toggle
- Add the sidebar screenshot ahead of the existing transcript screenshot
Applied to en/zh/ja/ko.
Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
de902a4aab |
MUL-6273: feat(agent): add MiniMax Code ACP runtime (#7035)
* feat(agent): add MiniMax Code ACP runtime * fix(agent): terminate mcode process trees * fix(agent): address mcode review blockers * test(agent): keep mcode model contract API-stable * fix(migrations): resolve mcode prefix collision * fix(agent): launch mcode through runtime command Assisted-by: MiniMax Code * fix(migrations): renumber mcode migration Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9a30c51dac |
MUL-6286: feat(properties): actor / multi_actor custom property types (members) (#7075)
* feat(properties): add actor / multi_actor custom property types (MUL-6286) Custom issue properties gain their first reference type: `actor` (single) and `multi_actor` (multiple), holding workspace members and agents. Value shape is a prefixed string — "member:<user_id>" / "agent:<agent_id>", multi being an array of them. Keeping it a plain string rather than an object means the existing @> containment filter, the jsonb_path_ops GIN index and the client value schema all keep working unchanged; an object-shaped value would have been silently dropped by the client preprocess that guards against unknown value shapes. Scope decisions: - Squads are out. The assignee pair accepts member/agent/squad, but a squad is a routing target rather than a person. The stored form carries the kind, so widening later needs no migration and no new type. - Referencing an agent is NOT an assignment: nothing is enqueued, routed or notified. The picker therefore does not gate agents on runtime binding or invoke permission the way the assignee picker does — it gates on visibility, matching the server. - Agent identity still needs protecting, so writes and facet reads both run through accessibleAgentIDs: a private agent never becomes referenceable or discoverable by id. - multi_actor dedupes but preserves insertion order instead of canonicalizing like multi_select. There is no config order to sort against, and sorting by id would reshuffle the avatar row on every edit. @> containment is order-insensitive, so filtering is unaffected. - Actor properties are filterable (including "= me", with the signed-in member sorted first) but not sortable or groupable — the same position multi_select already holds. Both types ship together because a property's type is immutable: shipping single-only would have forced anyone wanting multi to recreate the property and re-enter its values. Co-authored-by: multica-agent <[email protected]> * refactor(properties): narrow actor properties to members only (MUL-6286) Scope change after review: `actor` / `multi_actor` hold workspace members only. Agents are out — a passive reference had to answer agent-visibility questions (private / non-allow-listed agents must not become discoverable by id) and carry a "referencing is not assigning" caveat everywhere, all for a use case nobody has asked for. The original request was for members. The envelope is unchanged on purpose, so this stays a one-line widening later rather than a migration: the types are still named `actor` / `multi_actor`, the stored value is still the kind-prefixed "member:<user_id>", and the migration still allows both type names. Adding "agent" back means appending to `actorPropertyKinds` — existing definitions would gain the kind in place, with no new property type and no value rewrite. What this removes: - The agent branch of `resolveActorRefs`, including the accessibleAgentIDs visibility gate and the archived-agent check. - `filterAccessibleActorFacetValues` entirely. Facet keys are now all `member:<uuid>`, and workspace membership is already visible to every member, so there is nothing left to gate. - The agent section of the actor picker, and agents from the filter dropdown's candidate list. - The "reference, not an assignment" framing from the skill and product docs — that distinction only existed because agents were allowed, and keeping it would now confuse rather than clarify. `actorPropertyKinds` carries the note that any future kind which is not universally visible to every member must restore the visibility gate on both the write path and the facet read path. Tests updated to match: agent refs are now rejected at the kind check, and the private-agent visibility test is replaced by TestIssueActorPropertyFacets, which covers the facet path that backs the "= me" filter. Co-authored-by: multica-agent <[email protected]> * fix(properties): address actor-property review findings (MUL-6286) Four defects from review, plus one pre-existing bug found next to them. 1. Editing a multi_actor value silently deleted kinds the client did not recognise. The picker rebuilt the array from `actorRefsFromValue`, which filters unknown kinds — so once the backend widens actorPropertyKinds, an installed desktop build would drop every agent reference the moment the user toggled a member. The edit path now round-trips the RAW string list via the new `actorRefValuesFromValue`, and the toggle is extracted to `toggleActorRefValue` so the guarantee is directly testable. 2. Actor ids were stored exactly as received. `uuid.Parse` also accepts uppercase, braces and the urn: form, and every consumer (member directory lookup, "= me" filter, @> containment) compares reference strings exactly — so `member:<UPPERCASE-UUID>` passed validation and then rendered as Unknown and matched no filter. `parseActorRef` now stores the canonical form. 3. The CLI, docs and skill all promised member email as a way to address a value, but `resolveAssignee` only matched id and name. Email is now an exact-match alias ranked with id matches (it is as unambiguous as an id), so the documented behaviour is real. This also makes `--assignee` accept an email, which is a strict improvement. 4. An active actor-property filter rendered as a bare count. The chips bar loads each directory lazily and gated the member directory on assignee/creator filters only, then looked names up in `config.options`, which actor properties do not have. It now detects actor-typed filters, loads the directory, and renders names plus an avatar stack. Found while fixing 4: the chips bar keyed its actor-name map by the membership row id (`m.id`) while every actor filter value carries `user_id`, so member names never resolved in the assignee and creator chips either. One token, same expression, fixed here rather than left next to the new code. Also drops two comments still describing the member/agent scope. Co-authored-by: multica-agent <[email protected]> * fix(properties): stop a single actor value overwriting an unreadable ref (MUL-6286) Round-two review finding. The previous fix only protected `multi_actor`: its toggle round-trips raw strings, so a reference kind this build cannot parse survives an edit. Single `actor` had no such protection. Once the backend widens actorPropertyKinds, an older client reading `agent:<id>` in a single actor property would filter it out, render the field as empty, and then — when the user "filled in the empty field" — replace it outright. The user sees themselves setting a blank field; what actually happens is a value they were never shown gets deleted. The editor now degrades to read-only when a single actor value holds a reference it cannot parse, and the display says so instead of showing the empty-state label. Clear is still offered, so a genuinely stale value can be removed deliberately. `multi_actor` stays editable — its toggle is already non-destructive. The read-only decision moved into an exported `isCustomPropertyReadOnly`, so all three reasons it can fire (archived, unknown type, unreadable actor ref) are covered by tests rather than being inline in the component. Also closes the two non-blocking gaps from the same review: - The "email outranks a name substring match" CLI test asserted a ranking its fixture could not actually exercise — no candidate's name contained the email. Added an agent named "Mailer for [email protected]", so the test now fails if email stops being ranked with id matches. - Extracted `buildChipActorNames` and asserted a member resolves by `user_id` and NOT by the membership row id — the pre-existing bug fixed in the last commit now has a test that would catch its return. Co-authored-by: multica-agent <[email protected]> * chore(migrations): renumber actor-types migration to 341 (MUL-6286) 332 was taken by 332_issue_status on main while this PR was open; the migration lint test caught the collision in CI. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
2014ae3613 |
feat(skills): bulk update from source (#6930)
Rescoped from the original freshness-awareness PR per maintainer review on #6930: the upstream freshness tracking (import-time version markers, the refresh-check endpoint and cache, and the detail-page up-to-date indicator) is dropped entirely; only the bulk update remains, simplified. The skills list batch toolbar gains an Update action: the confirm dialog shows how many selected skills will be re-downloaded from their hosted source and how many are skipped (manual/runtime/no permission), then reuses the existing single-skill POST /api/skills/:id/refresh sequentially across the updatable selection. No freshness probe first — re-fetching an already-current skill costs one download on an explicit click. Kept from the original design: per-item failure tolerance (one 409 must not strand the batch), a partial-failure toast, and the selection surviving a partial run so retry is one click. Frontend-only; no API or schema changes. Co-authored-by: Guillaume Sayer <[email protected]> |