Skip to content

Merge from upstream - #3

Merged
0xHexE merged 15 commits into
anthaathi:mainfrom
multica-ai:main
Jul 6, 2026
Merged

Merge from upstream#3
0xHexE merged 15 commits into
anthaathi:mainfrom
multica-ai:main

Conversation

@0xHexE

Copy link
Copy Markdown
Member

What does this PR do?

Related Issue

Closes #

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Refactor / code improvement (no behavior change)
  • Documentation update
  • Tests (adding or improving test coverage)
  • CI / infrastructure

Changes Made

How to Test

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots
  • I have updated relevant documentation to reflect my changes
  • If I added a new runtime / coding tool / UI tab, I synced the change to landing copy (apps/web/features/landing/i18n/) and relevant docs (apps/docs/content/docs/)
  • If this PR touches Chinese product copy, I checked it against apps/docs/content/docs/developers/conventions.zh.mdx (terminology, mixed-rule for task / issue / skill)
  • I have considered and documented any risks above
  • I will address all reviewer comments before requesting merge

AI Disclosure

AI tool used:

Prompt / approach:

Screenshots (optional)

NevilleQingNYand others added 15 commits July 6, 2026 08:43
* MUL-4016: fix mention tokenizer stacktrace backtracking
Co-authored-by: multica-agent <github@multica.ai>
* fix(editor): de-ambiguate escaped-label regexes to kill ReDoS (MUL-4016)
The mention/slash/file-card label regexes used `(?:\\.|[^\]])` where both
alternatives can consume a backslash. On an unterminated match, each `\x`
run is enumerated 2^n ways — pasting a Java stacktrace (`\~\[...\]`) or a
crafted ~50-char string freezes the main thread for seconds (GitHub #4881).
Exclude backslash from the char class (`[^\]\\]`) so a backslash can only be
consumed by `\\.`. The alternatives become disjoint and matching is linear;
legal escaped-bracket labels like `David\[TF\]` still parse unchanged.
Fixed in all four sites that shared the pattern:
- mention-extension.ts tokenize()
- slash-command-extension.ts start() + tokenize()
- file-card.tsx FILE_CARD_MARKDOWN_RE
- packages/ui/markdown/file-cards.ts NEW_FILE_CARD_RE (runs on every
read-only comment/description render, not just the editor)
Adds adversarial regression tests (repeated `\a` + missing closing bracket)
that fail in ~10-40s against the old regexes and pass in <1ms after the fix.
Builds on #4889's marker-first mention start().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(editor): escape backslash in mention/slash labels for round-trip (MUL-4016)
Follow-up to the de-ambiguation fix, addressing Howard's PR review.
The linear tokenizer now treats "\" as an escape lead (\\.), so a label whose
serialized form contains a bare "\" adjacent to the closing "]" no longer parses
back — the "\]" is consumed as an escaped bracket and swallows the boundary. The
old ambiguous regex tolerated this by chance; the de-ambiguation exposes it.
mention/slash renderMarkdown escaped only [ and ], not \. Switch both to the
shared escapeMarkdownLabel() (escapes [ ] \ ( )) and mirror it on parse with
replace(/\\([[\]\\()])/g, "$1"), matching what file-card already does. This also
converges the three tokenizers on one escape contract. file-card was already
correct and is unchanged.
Adds parameterized round-trip tests for labels containing "\" / "\]" / parens
(e.g. "A\\", "ends\\", "a\\]b"); these fail on the old serializer and pass now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ons (#4962)
The FileUploadButton component already fans out per-file onSelect
callbacks and every editor surface already handles N concurrent
uploads (drag-drop and paste were multi-file all along), but five
call sites never passed the `multiple` prop, so the OS file dialog
capped picks at one file: chat composer, create-issue modal,
quick-create modal, issue description, and feedback modal.
Fixes MUL-4074.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
…59) (#4925)
* fix(search): add pg_trgm index fallback + statement_timeout guard (MUL-4059)
Root cause of the "search freezes with no response" symptom reported in
MUL-4059: the search handler runs LOWER(col) LIKE '%pattern%' queries
that expect a pg_bigm GIN index (migrations 032, 033, 036), but every
migration wraps the CREATE EXTENSION + CREATE INDEX in a DO/EXCEPTION
handler that silently skips when pg_bigm is unavailable. The bundled
self-host / dev / CI Postgres image (pgvector/pgvector:pg17) does not
ship pg_bigm, so on every self-hosted deployment the migrations no-op
and no GIN indexes get built. Every /api/issues/search + /api/projects/search
request then falls back to a Seq Scan on `issue` + correlated Seq Scans
on `comment` — verified with EXPLAIN on the local dev DB, which has zero
title/description/comment search indexes before this change.
Two independent guardrails are added, either of which alone would have
prevented the reported hang:
1) Migration 134 installs pg_trgm (ships in all standard Postgres +
pgvector images) and builds GIN indexes with gin_trgm_ops on
`LOWER(title)`, `LOWER(COALESCE(description, ''))`, and
`LOWER(content)`. The expression signatures match the search
handler's WHERE clauses exactly, so the planner picks the index
without further changes. The pg_bigm indexes from 036 are left
intact — deployments on AWS RDS with pg_bigm 1.2 keep the CJK-friendly
bigram path; deployments without it get the trigram fallback. Verified
against a local 25k-row fixture: the description LIKE hits
`Bitmap Index Scan on idx_issue_description_trgm` in 0.5 ms.
2) runSearchQuery wraps both search handlers in a short-lived read-only
transaction with SET LOCAL statement_timeout = 3 s. In the pathological
case where indexes are still missing or the query plan is bad, callers
see a fast 503 with a descriptive error instead of a stalled request.
Verified against a live Postgres: a deliberate pg_sleep(2) with the
test override at 200 ms is cut off in 230 ms with SQLSTATE 57014, as
asserted by TestRunSearchQuery_StatementTimeoutFires.
Non-goals: this change does not remove the pg_bigm code path, does not
change the SQL the handler builds, and does not change the API response
shape. It is the minimum diff to unblock production while preserving
the CJK-search advantage that pg_bigm provides where it is available.
Co-authored-by: multica-agent <github@multica.ai>
* fix(search): scope comment subqueries to workspace to unblock prd hang (MUL-4059)
Follow-up correction after PRD investigation: pg_bigm IS installed on
prd `multica-prod` and all five bigm indexes exist in the correct
`LOWER(...) gin_bigm_ops` form. The initial "missing index" hypothesis
was wrong; migration 134 (pg_trgm fallback) still helps self-host but
does not touch the production hang path.
Actual prd EXPLAIN (workspace with 60k issues, keyword "search"):
Index Scan using idx_issue_workspace on issue i
Rows Removed by Filter: 59123
SubPlan 2
Bitmap Heap Scan on comment c
Rows Removed by Index Recheck: 1928275
Heap Blocks: exact=48297 lossy=164696
Bitmap Index Scan on idx_comment_content_bigm
rows=536761
Execution Time: 32345.002 ms
Root cause: the correlated `EXISTS` over `comment` gets rewritten by
the planner into a *hashed* subplan. Without a workspace_id filter in
the subquery, that hashed set covers every comment in every workspace
matching the LIKE — 536k rows for "search" — which spills work_mem
into a lossy bitmap and rechecks 1.9M rows.
Two-part fix:
1. Query rewrite. buildSearchQuery now emits
`c.workspace_id = $wsParam` inside every comment subquery (WHERE
phrase match, WHERE multi-term match, tier 7 rank, tier 8 rank, and
the matched_comment_content COALESCE). The same $4 parameter is
reused so Postgres treats it as a compile-time constant and pushes
it into the hashed subplan's key, collapsing the set to this
workspace's comments.
2. Supporting index (migration 135). New
`idx_comment_workspace ON comment (workspace_id)`. Without it, the
pushed-down filter still triggers a Seq Scan on `comment` because
comment has no btree on workspace_id (only the FK constraint and
composite (issue_id, ...) indexes).
Locally verified against a repro that mirrors prd (5k issues in the
target workspace, 100k comments in a sibling workspace all containing
"search"): the plan drops from 60 ms (hashed global scan, no support
index) to 3 ms (subplan uses idx_comment_workspace). Prd extrapolation
from the same shape: 32.3 s → tens of milliseconds.
Regression test TestBuildSearchQuery_CommentSubqueryWorkspaceScope
asserts every `FROM comment c` in the generated SQL is followed by a
`c.workspace_id = $4` filter, so a future refactor can't silently
regress the plan back to the global-hash pathology.
The statement_timeout guard from the earlier commit in this branch is
kept — it still bounds the worst case if any future query shape
regresses.
Co-authored-by: multica-agent <github@multica.ai>
* fix(search): address PR review — unwrap 135 + add project trigram indexes (MUL-4059)
Both must-fix items from GPT-Boy's review:
1. Migration 135 unwrapped. The previous version buried
`CREATE INDEX idx_comment_workspace` inside `DO $$ ... EXCEPTION
WHEN OTHERS $$` — exactly the anti-pattern that caused MUL-4059 in
the first place. `idx_comment_workspace` is not a CJK-bonus fallback;
it is the critical support that makes the query rewrite land on an
Index Scan instead of a Seq Scan. A silent failure (lock timeout,
disk full, permission denied, schema drift) MUST abort the migration
and fail deployment, not slip through as green. The unwrapped
`CREATE INDEX IF NOT EXISTS` now propagates real errors to the
migration runner, which aborts and does NOT record the version as
applied. IF NOT EXISTS keeps idempotency for the operator-precreated
case (`CREATE INDEX CONCURRENTLY ...` before running migrations on
large prd tables).
2. Migration 134 now covers project search too. SearchProjects reads
`LOWER(project.title)` and `LOWER(COALESCE(project.description, ''))`,
and the pg_bigm equivalents in migration 039 silently no-op on
pg_bigm-less images just like 032/033/036. Without the trigram
fallback, project searches on self-host would still Seq Scan and hit
the 3 s statement_timeout guard as a 503 — technically bounded but
not actually fixed. Added `idx_project_title_trgm` and
`idx_project_description_trgm`; the down migration drops them too.
Also: fixed the search.go comment that said callers get a "standard
500" — they get a 503 with SQLSTATE-57014 mapping; the comment now
matches reality.
Verified: build clean, vet clean, existing search / timeout tests
still green. Migration 135 dry-run (dropping the index, re-applying
the unwrapped SQL under `ON_ERROR_STOP=1`) creates the index cleanly;
a deliberate `CREATE INDEX` on a non-existent column now aborts psql
with exit 3, confirming the migration runner would fail loudly on any
real error.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: JC的AI分身 <tangyuanjc@JCdeAIfenshendeMac-mini.local>
Co-authored-by: multica-agent <github@multica.ai>
Trae (traecli) already has a New() backend, launch header (traecli acp
serve) and provider branding, but was missing from every protocol_family
whitelist, so custom runtime profiles based on Trae were rejected and it
never appeared in the family picker.
Add traecli to SupportedTypes (Go), RUNTIME_PROFILE_PROTOCOL_FAMILIES
(TS), the lockstep test's want map, and a new migration 136 widening the
runtime_profile_protocol_family_check constraint.
MUL-4094, #4945
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
…UL-3972) (#4847)
The issue action menu (3-dot / right-click) nested a "More" submenu inside the
already-open menu, so opening the menu surfaced yet another "More" — the first
level told you nothing about what was inside.
Rename that submenu to the semantically explicit "Relations" (关系 / 関係 / 관계)
with a Network icon, matching the noun-labelled pattern of the sibling submenus
(Status, Priority, Start date, Due date). Its contents are unchanged — create/add
sub-issue and set/remove parent — and stay grouped so future relation types
(blocks, duplicates, related) have a home.
- Rename i18n key actions.more -> actions.relations across en/zh-Hans/ja/ko
- Swap MoreHorizontal icon for Network
- Update the shared menu test
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
… (MUL-4030)
* fix(agent): pi agent final output excludes intermediate steps
Updated PI agent to only retain the final result in JSON output.
Previously, `text_delta` included both intermediate steps and final
content.
Now, output is reset on each `text_start` to concatenate only the
final text.
* fix(agent) Replace `message_update.text_start` with `turn_start` event. add test
`turn_start` begins a new turn, Reset output on it to exclude
intermediate texts.
https://github.com/earendil-works/pi/blob/a1b336d73e13b53949ff629800081185d3e4694e/packages/coding-agent/docs/rpc.md#events
Update public docs and landing copy to the current 14-runtime list; add Qoder / Trae CLI across localized docs. Follow-up: finish JA tool counts (tasks.ja / skills.ja) and align localized Trae section anchors with the /providers#trae links.
Closes#4945
Co-authored-by: vicksiyi <zeroicework@163.com>
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
* fix: harden Windows browser MCP config
Co-authored-by: multica-agent <github@multica.ai>
* fix: address browser mcp review nits
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
…107) (#4978)
The server-side running-task sweeper failed rows purely on `started_at >
now() - runningTimeoutSeconds` (2h30m). By its own comment the wall clock
is "mainly for runs whose daemon died without reporting" and "only needs
to sit generously above any realistic single run" — but the predicate
does not actually distinguish a healthy long-running task from an
orphaned one. On self-hosted deployments this kills multi-hour research
/ training runs mid-flight even though the daemon is still heartbeating
and the run is actively producing output.
The daemon side is intentionally unbounded (only inactivity watchdogs:
idle 30m, tool 2h); the server backstop was silently the only wall
clock. `FailStaleTasks` is now AND-gated on runtime liveness:
* dispatched — unchanged; already excludes rows with a live
`prepare_lease_expires_at` (renewed every 15s by the daemon between
claim and StartTask).
* running — new: excluded when the task's `agent_runtime` row is
`online` AND `last_seen_at` is within the runtime stale window
(staleThresholdSeconds = 150s, the same signal
sweepStaleRuntimes already uses).
Healthy long-running tasks on live daemons are no longer killed by the
wall clock. The daemon-dead case remains primarily handled by
sweepStaleRuntimes in the same tick (Redis LivenessStore + DB stale +
FailTasksForOfflineRuntimes); the wall-clock branch is now a defensive
backstop for the pathological case where a runtime row lingers online
with a stale DB heartbeat for longer than the wall clock. `runtime_id
IS NULL` is treated as "not proving liveness" so the wall clock still
fires on that (rare / historical) shape.
The 2h30m default is unchanged — this is a gate, not a threshold
change. Tests updated: 4 existing running-task tests now age out the
runtime so they still exercise the wall clock; 2 new tests cover both
new invariants (healthy runtime → skipped; stale runtime → still
killed).
Fixes#4958
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
…g (MUL-4129) (#4980)
The delete-workspace flow navigates away before awaiting the DELETE
(required ordering — see navigateAwayFromCurrentWorkspace's
CancelledError notes), but useDeleteWorkspace left the workspace in the
list cache until onSettled. During the pending window any list refetch
re-presented the deleting workspace as a selectable/current option.
Optimistically remove it in onMutate (after cancelling in-flight list
fetches), roll the snapshot back in onError so a failed delete restores
the workspace alongside the existing error toast, and keep the
onSettled invalidate as the server-truth reconcile.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
@0xHexE
0xHexE merged commit 4af90a1 into anthaathi:mainJul 6, 2026
6 checks passed
0xHexE pushed a commit that referenced this pull request Jul 9, 2026
…UL-4195) (multica-ai#5068)
* fix(comments): guarantee at-least-once processing of user comments (MUL-4195)
Consecutive comments on an issue were silently dropped: a new comment that
arrived while the agent already had a queued/dispatched task was discarded by
the HasPendingTaskForIssueAndAgent dedup, losing the user's follow-up
instruction with no visible trace. Comments — unlike chat — are deliberate,
addressed, persisted input and must never vanish.
This makes comment handling at-least-once while keeping concurrency bounded to
one run per (issue, agent):
- Merge, don't drop (PR1): a comment landing while a not-yet-started task
exists is folded into that task — the prior trigger becomes a coalesced
comment and the new one becomes the trigger, so a single run still covers
every deliberate comment. Falls back to a fresh enqueue if the pending task
was claimed mid-flight, so nothing is lost in the race.
- Completion reconciliation (PR2): on task completion, a member comment newer
than the run's started_at schedules exactly one follow-up via the normal
trigger pipeline. Loop-safe: member-authored only, capped by the existing
per-(issue,agent) dedup, and terminating.
- Visibility (PR3): coalesced_comment_ids is surfaced on the task API and in
the run prompt so the covered comments are explicit.
Migration 145 adds agent_task_queue.coalesced_comment_ids UUID[].
Tests: merge-not-drop preserves all three of a rapid burst and repoints the
trigger to the newest; reconciliation query gates on member/since; e2e
CompleteTask enqueues a follow-up for a mid-run member comment and does not for
none.
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): address review — originator gate, agent-scoped reconcile, cross-thread coalesced prompt (MUL-4195)
Resolves GPT-Boy's Request-changes review on PR multica-ai#5068.
Must-fix#1 — merge no longer inherits a stale originator/runtime context.
MergeCommentIntoPendingTask now only folds a comment into a pending task
whose originator_user_id IS NOT DISTINCT FROM the new comment's originator.
runtime_mcp_overlay / runtime_connected_apps are a pure function of
(originator, agent) and the agent is fixed, so a matching originator keeps
the stored overlay/attribution valid; a differing originator (e.g. user B
commenting on a task originated by user A) matches no row and the caller
enqueues a fresh follow-up with B's own context instead of reusing A's.
trigger_summary is refreshed to the new trigger comment.
Must-fix#2 — completion reconcile no longer re-wakes unrelated agents.
reconcileCommentsOnCompletion computes the latest member comment's triggers
and keeps ONLY the agent that just completed, instead of fanning the comment
out through the full pipeline. An @-mention of agent B during agent A's run
is triggered once at creation time and is no longer replayed (double-run)
when A completes.
Should-fix#3 — coalesced-comment prompt no longer assumes a single thread.
The claim response now carries each folded comment's thread id / author /
created_at / content (CoalescedCommentData); the prompt embeds them directly
so the agent addresses cross-thread folded comments without the wrong
"they are in the triggering thread" hint. Old servers that ship only ids
fall back to an issue-wide fetch, still without the same-thread assumption.
Tests: TestMergeCommentIntoPendingTask_OriginatorGate (query gate),
TestCompleteTask_DoesNotReTriggerOtherAgentMentionedDuringRun (reconcile
scoping), TestBuildCommentPromptCoalescedCrossThread / IDsOnlyFallback
(prompt). Existing MUL-4195 suites still pass.
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): close unique-index drop + dispatched-window race in comment coalescing (MUL-4195)
Second-round review follow-up on PR multica-ai#5068.
Must-fix#1 — originator-mismatch no longer drops the comment.
The previous originator gate returned ErrNoRows on a different originator and
the caller fell through to a fresh enqueue, which collided with the
idx_one_pending_task_per_issue_agent unique index (one queued/dispatched task
per (issue, agent)) — silently dropping the second user's comment. Replaced
the gate with recompute-on-merge: MergeCommentIntoPendingTask now re-stamps
originator_user_id, runtime_mcp_overlay, runtime_connected_apps and
trigger_summary to the new comment's originator. A different member's comment
folds into the single coalescing run carrying the latest instruction's own
identity/overlay (no cross-user capability bleed, no drop, no collision).
Must-fix#2 — comment arriving in the claim→StartTask window is no longer lost.
Merge now targets only PRE-CLAIM states ('queued','deferred'); a
dispatched/running task is never a merge target, so a post-claim comment is
never falsely stamped into coalesced_comment_ids as "delivered". Completion
reconcile is re-anchored on dispatched_at (the moment the claim response is
built) instead of started_at, and sweeps ALL undelivered member comments since
that anchor — replaying each through the normal enqueue path so they coalesce
into one bounded, agent-scoped follow-up run. This covers the dispatch→start
window a started_at anchor missed.
Enqueue path: on a merge miss the caller no longer blindly fresh-enqueues
(which could collide with a dispatched sibling); it defers to the active
task's completion reconcile via HasActiveTaskForIssueAndAgent, and only
fresh-enqueues when no active task exists.
Tests: rewrote the query test to
TestMergeCommentIntoPendingTask_RecomputesOriginatorAndSkipsDispatched;
added TestConsecutiveCommentsDifferentOriginatorsFullEnqueuePath (full handler
enqueue path, two distinct originators) and
TestCompleteTask_ReconcilesDispatchedWindowComment (claim→start window). All
existing MUL-4195 handler/cmd-server/daemon/service suites still pass.
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): catch pre-dispatch merge-race comment in completion reconcile (MUL-4195)
Third-round review follow-up on PR multica-ai#5068.
Race: a member comment is created while the task is still queued, but its
merge loses the race to the daemon claiming the task (queued→dispatched). The
merge then finds no pre-claim row (ErrNoRows), the enqueue path defers to
reconcile — but the comment's created_at is BEFORE dispatched_at, so the
dispatched_at-anchored reconcile skipped it and the comment vanished with no
task coverage.
Fix: anchor completion reconcile on the task's created_at (which always
precedes dispatch) instead of a dispatch/start timestamp, and exclude the
run's DELIVERED SET — trigger_comment_id ∪ coalesced_comment_ids. Because
merges only ever touch pre-claim rows, that set is exactly what the claim
response carried, so any member comment created since the task was made that
is NOT in it was genuinely undelivered and earns a bounded follow-up. This
catches the pre-dispatch merge-race comment and the dispatch→start comment,
while never re-firing a comment that was delivered as a pre-claim coalesced
entry.
Test: TestCompleteTask_ReconcilesPreDispatchMergeRaceComment reproduces the
race (comment created pre-dispatch, task dispatched before merge, plus a
delivered coalesced comment) and asserts exactly one follow-up, triggered by
the race comment, with the delivered coalesced comment excluded. Existing
reconcile fixtures updated to set a realistic created_at (the production
invariant that created_at is the earliest task timestamp).
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): merge only into the queued task, never a deferred fallback (MUL-4195)
Fourth-round review follow-up on PR multica-ai#5068.
MergeCommentIntoPendingTask targeted status IN ('queued','deferred') ordered
by created_at DESC. When a (issue, agent) pair had both an older queued task
(the run about to be claimed) and a newer deferred assignee-fallback task, a
new comment merged into the deferred row instead of the queued one — so the
comment missed the imminent run and the deferred fallback could later promote
into a duplicate/conflicting run.
This merge is only ever reached when HasPendingTaskForIssueAndAgent matched a
queued/dispatched task (it never inspects deferred), so the coalescing target
must be the queued row. Restricted the merge target to status = 'queued'
(the unique index guarantees at most one). Deferred fallbacks keep their own
fire_at/promotion escalation lifecycle and are never a merge target.
Test: TestMergeCommentIntoPendingTask_TargetsQueuedNotDeferred seeds an older
queued task + a newer deferred fallback for the same (issue, agent), merges a
new comment, and asserts it lands on the queued task (trigger repointed, old
trigger coalesced) while the deferred fallback is left untouched.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
0xHexE pushed a commit that referenced this pull request Aug 4, 2026
…from the sidebar (MUL-5465) (multica-ai#6132)
* feat(issues): Issue Quick Actions — preset agent + prompt, one-click from the sidebar (MUL-5465)
Preset "who to call and what to say" once in Settings, then trigger it from
any issue's sidebar with a single click.
Running one is NOT a new dispatch path. The server renders the prompt, posts
a `quick_action` comment carrying the target's mention markup, and hands off
to the existing comment -> mention -> task trigger. Permission
(canInvokeAgent), attribution, squad-leader routing, the execution log, and
pending-task coalescing are inherited rather than reimplemented — the
MUL-3375 lesson about four drifting copies of one trigger decision.
Three things the UI has to be honest about, because the backend already
decided them:
- One pending task per (issue, agent) is a DB invariant
(idx_one_pending_task_per_issue_agent). A second click against a busy agent
starts no new run; the comment merges into the pending task. The toast says
"Added to Lambda's current run", not "Lambda started working".
- An offline target defers rather than fails; the run reuses the existing
dispatch.ReasonCode vocabulary instead of inventing one.
- Private agents are deny-by-default with no admin bypass. The sidebar filters
by the caller's own invoke verdict, so a dead button is never rendered, and
a direct API call still 403s with `invocation_not_allowed`.
Visibility is DERIVED from the bound agent's permission_mode on every request,
never stored — so it cannot drift after someone flips an agent between private
and public_to. Binding a workspace action to a private agent is allowed (the
alternative pressures people into making agents public just to satisfy a
config constraint) but the settings form says so at bind time, and the
catalog badges it. The target's name is withheld from callers who cannot see
it, so the response never discloses a private agent's existence.
Prompt templating is flat substitution over a closed whitelist. No
conditionals, loops, or filters — the agent already reads the whole issue, so
natural language is the control flow. One optional runtime input ({{input}})
keeps a single action from splitting into five near-identical variants; both
directions of the input/{{input}} agreement are rejected at write time so a
typo can never land silently.
Surfaces: sidebar (top 5, rest behind More), the `/` menu in the comment
composer (inserts the server-rendered body to edit before sending), and
Alt-click for the same hand-off from the sidebar.
Migrations 234-236: quick_action table, its listing index (CONCURRENTLY, own
file), and comment.type + comment.quick_action_id.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(issues): simplify quick action permissions to a stored public/private intent (MUL-5465)
Replaces the derived four-value visibility model with a two-value choice made
at creation, and collapses permission handling to a single check.
The old model computed visibility per request from the bound agent's
permission_mode and used it to filter the sidebar. That filtering was the
problem: two people on one issue saw different sidebars with nothing to
explain the difference, which is harder to debug than a button that tells you
why it refused. It also required the list endpoint to run an invocation-target
query per action per request.
Now:
- `visibility` is stored INTENT — 'public' or 'private' — chosen up front.
- A public action must bind a target every workspace member can invoke
(public_to carrying a workspace target), enforced at write time. So a
public action is runnable by construction and dead buttons are eliminated
at the source rather than filtered out later.
- A private action allows any target and is returned only to its creator.
That scoping is what the field MEANS, not a permission check.
- Permission is checked in exactly one place: RunQuickAction. A refusal is a
structured 403 the client renders as one dialog. The dialog does not
distinguish "no permission" from "the binding drifted" — the person
reading it takes the same next step either way, and the person who can fix
it looks at settings.
Removed: can_run, position + manual ordering (settings sorted by usage while
the sidebar sorted by position — one list, two orders), the derived
visibility_broken flag, the runnable_only projection and its second cache
entry, target_name redaction, the alt-click composer hand-off (the `/` menu
covers insert-then-edit and is discoverable), and the sidebar_limit response
field (now a shared constant).
Ordering is use_count DESC everywhere. Settings shows the target's current
reachability as plain metadata ("Nova · private"), so a public action pointing
at a now-private agent reads as visibly wrong without a bespoke error state.
The tradeoff — no active signal when that drift happens — was accepted
deliberately: drift is rare and the failure is loud at click time.
Migration 234 is edited in place rather than layered, since the PR is
unmerged and the table has never been deployed.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(issues): drop quick action variables and runtime input (MUL-5465)
V1 ships a preset prompt sent verbatim, triggered from the sidebar or the `/`
slash command. Two features are removed and one guard is kept.
Runtime input goes because `/` already covers it. Typing `/code review` drops
the rendered body into the composer, where any part of it can be edited before
sending — strictly more flexible than one fixed field, and the field was
specified before `/` was in V1. Two UIs for one need.
Variables go because none of them passed their own test. The rule was that a
variable earns its place only if it changes what the agent ATTENDS TO, not what
it KNOWS. Checked one by one — {{issue.title}}, {{issue.identifier}},
{{issue.url}}, {{user.name}}, {{date}} — the agent already has every one from
the issue context and from the fact that the comment is authored by the person
who triggered it. They were inherited from autopilot's title template rather
than justified.
The REJECTION survives the feature: any `{{...}}` is refused at write time,
naming the offending token. Someone carrying the habit over would otherwise
have `{{issue.title}}` rendered literally into an agent's instructions and
never notice — the exact silent-typo failure the whitelist existed to prevent.
The check is a fraction of the interpolation engine it replaces and keeps the
door open to enabling variables later without touching stored data.
Removed: 4 columns (input_enabled/label/placeholder/required),
renderQuickActionPrompt + the variable whitelist + quickActionIssueURL, the
two-way {{input}} agreement logic, the run/render `input` parameter, the
variable insert chips, the entire "Ask for input on click" block, and the
sidebar's Popover branch — every row is now a plain button. The settings
dialog drops from six field groups to four.
Migration 234 is edited in place rather than layered, since the PR is unmerged
and the table has never been deployed.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(settings): align Quick Actions with the Labels/Properties list, then fix what the UI review found (MUL-5465)
The tab used a bespoke card list while its two siblings — Labels and
Properties — share one table layout. These three are the workspace's catalog
of small named things and should read as one surface, so Quick Actions now
uses the same structure: search + primary action row, bordered card, responsive
column grid that collapses to stacked rows under `md`, and an overflow menu
instead of a row of icon buttons. Columns are Name / Runs as / Who / Used /
Updated. The tab joins the max-w-5xl group for the same reason.
A UI review pass over the result found five things, four of which are fixed
here:
- The visibility chooser communicated selection through border and background
only, so a screen reader announced both options identically. Added
aria-pressed.
- The editor dialog was max-w-xl while both siblings use sm:max-w-lg, and the
unprefixed cap applied at every breakpoint.
- The empty-state hint diverged from the Properties tab it was copied from
(text-sm and no max width vs mx-auto max-w-sm text-xs).
- Two hardcoded `text-amber-600 dark:text-amber-400` usages replaced with the
`text-warning` semantic token, per the repo's design-token rule.
Also fixed a signal-quality bug the review surfaced: the usage column
highlighted anything with use_count 0, so an action was flagged the instant it
was created. Staleness now means "has had time to be used and wasn't" — 90
days since last use, or 90 days since creation for one never used.
Not fixed here: the overflow trigger is size-7 (28px), under the 44px touch
floor. Labels and Properties use the identical size, so changing only this tab
would break the consistency this commit exists to create; it needs one pass
across all three.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): drop the quick_action comment type, widen the mention guard, harden the slash race (MUL-5465)
Second review round on PR multica-ai#6132. All four remaining findings.
**Comment type removed entirely (#2 blocker + #3).** Adding a `quick_action`
type meant dropping and re-adding comment_type_check, and re-adding a CHECK
holds ACCESS EXCLUSIVE on `comment` for a full table scan — a read/write stall
on one of the hottest tables in the product, every deploy. It was also
forgeable: `type` is client-supplied on POST /comments, so any member could
post type='quick_action' and have an ordinary comment render as an action
audit record with its body collapsed out of view.
Both go away by not having the type. A quick action now posts an ORDINARY
comment marked with `quick_action_id`, and the collapsed card keys off that id.
There is no request field for it, so the marker cannot be forged, and the
migration is a bare nullable ADD COLUMN — metadata-only and instant. Verified
against a fresh database: comment_type_check is untouched.
The generic comment endpoint now also validates `type` instead of letting the
DB CHECK reject it. An unknown type surfaced as a 500 on a constraint
violation, which reads as a server fault for plainly bad input; it is a 400
now. `status_change` and `system` are excluded from what a client may author —
claiming those would be forging system narration.
**Member mentions rejected too (#1).** The first pass allowed
`mention://member/...` in prompts on the reasoning that it "only renders a
link". That was wrong: notification_listeners.go adds member mentions to the
recipient set and creates an inbox item, so a saved prompt pinged that person
on every single click. Only `mention://issue/...` reaches nobody and stays
allowed.
**Slash race, properly this time (#4).** The previous fix checked only that the
range still started with "/". Rewriting `/review` into `/fix` while the request
was open passed that check, and the stale response overwrote the new command.
The exact original text is now captured and compared; if the command was
edited, moved, or removed, the pick is abandoned rather than inserted
somewhere wrong. Adds the three regression tests the review asked for:
delayed resolve, rejection, and edit-during-flight.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): stop the quick action card repeating its own prompt, and insert the `/` body as markdown (MUL-5465)
Two fixes, one reported and one found while verifying it.
**The card printed the prompt twice.** The collapsed header previewed the
prompt's first line, and expanding showed the mention line plus that same
prompt again. The header now identifies WHICH action ran — "Code Review via
Lambda" — which is both non-redundant and something the body never told you:
the prompt text alone does not say which action produced it. This is what the
original design called for; previewing the prompt was the implementation
drifting from it.
When the action cannot be resolved — deleted, or another member's private one
and so absent from this viewer's catalog — the header falls back to the
prompt's opening line, which is the previous behaviour.
**The `/` menu inserted its body as literal text.** insertContentAt was called
with a plain string, so Tiptap treated the server-rendered markdown as text
rather than parsing it. The mention never became a node; it serialised back out
with escaped brackets (`\[@lambda\](mention://agent/…)`) and rendered as raw
markup in the thread. Passing `contentType: "markdown"` — the same option the
description editor already uses — parses it properly. Found by reading the
comment rows while checking the first fix: one had escaped brackets and no
quick_action_id, which is what a slash-inserted comment looked like.
The existing async test now asserts the contentType, so the option cannot be
dropped again without failing.
Co-authored-by: multica-agent <github@multica.ai>
* docs(issues): correct the stale quick actions sidebar comment (MUL-5465)
The comment still claimed the section renders nothing when no action is
runnable by the member. Permission filtering was removed several rounds
ago -- the list is deliberately unfiltered and a refusal is explained at
run time -- so the comment described behavior that no longer exists.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(settings): cut the quick action dialog's helper copy in half (MUL-5465)
The dialog had five blocks of explanatory prose around four fields, and
three of them wrapped to two lines, so the form read as a paragraph with
inputs in it.
Each helper now earns its line or loses it:
- The header explained the implementation ("keeps the same history,
permissions, and execution log as an @mention") -- an architecture note
the person creating an action does not need. Reduced to the one fact
they do: it posts a comment.
- "Who can use it" is a question, so the hints answer it as noun phrases
("Everyone in the workspace" / "Only you") instead of restating the
verb. Both now fit one line, which also makes the two cards the same
height -- the shorter one used to sit in dead space.
- The target and prompt hints front-load the constraint rather than
burying it mid-sentence.
70 words to 32 across the dialog, with no fact dropped. Field spacing
goes 4 -> 5 so the gap between groups beats the gap inside one.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(issues): render a quick action comment as an ordinary comment (MUL-5465)
The card had a collapsed one-line header that expanded to reveal the
prompt, on the theory that repeated runs of the same action would bury
the discussion. That was solving a problem the feature does not have:
prompts are a sentence or two, the header restated what the body already
said, and the disclosure only put a click between the reader and the
text.
A quick action posts a real comment through the real mention path, so
the honest rendering is the one every other comment gets. Drops
QuickActionCommentBody, its query for the action catalog, and the
now-orphaned quick_action_ran_via string in all four locales.
quick_action_id stays on the comment: it is provenance, and it was never
the reason the card looked different -- keying the special rendering off
it is what is going away, not the record itself.
Co-authored-by: multica-agent <github@multica.ai>
* fix(settings): use the faint tone token for the empty-state icon (MUL-5465)
main added apps/web/app/text-contrast.test.ts, a guard that rejects
transparency standing in for a text tone. The empty-state Zap used
text-muted-foreground/60, which is exactly the pattern it forbids: an
alpha-dimmed tone lands at a different contrast on every surface it is
composited over, so it cannot be reasoned about the way a token can.
text-faint-foreground is the token the guard names for icons and glyphs.
The rule arrived on main after this branch's last merge, so local runs
never saw it -- CI tests the merge commit, which is why only CI caught
it. Merged main first so the branch is checked against the same rules.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
0xHexE pushed a commit that referenced this pull request Aug 4, 2026
* feat: add QwenPaw ACP backend support
Add qwenpaw as a supported agent backend. QwenPaw runs via `qwenpaw acp`
over stdio using the ACP (Agent Client Protocol) JSON-RPC 2.0, reusing the
hermesClient transport layer.
Changes:
- New backend: pkg/agent/qwenpaw.go (ACP stdio via hermesClient)
- Register in agent.SupportedTypes, New(), launchHeaders
- Daemon config: probe qwenpaw binary, QWENPAW_ARGS env var
- Display name 'QwenPaw', inline system prompt support
- Runtime config: AGENTS.md injection, .qwenpaw/skills/ discovery
- User-level skills via QWENPAW_HOME env var
- DB migration 224: add qwenpaw to protocol_family whitelist
* fix(ci): add qwenpaw to CLI guard names and rename migration 231
- Add 'qwenpaw' to scripts/agent-cli-command-names.txt (TestAgentCLIGuardCoversDefaultCommands)
- Rename migration to 231 to avoid collision with existing migrations (TestMigrationNumericPrefixesStayUniqueAfterLegacySet)
* fix: address Bohan-J review comments on qwenpaw backend (PR multica-ai#5986)
Blocking fixes:
1. config.go: Add qwenpaw CLI probe so daemon discovers the binary
2. models.go: Add qwenpaw case returning empty list (same as qwen)
3. qwenpaw.go: Fix resume path — use session/load (QwenPaw implements
load_session, not session/resume), use resolveResumedSessionID,
only set resumeRejected on session-not-found errors, fail the run
on set_model failure instead of silently continuing
4. daemon.go: Add qwenpaw→"QwenPaw" to runtimeDisplayNameOverrides
5. agent.go: Update error message to list qwenpaw
Non-blocking fixes:
6. qwenpaw_test.go: Add 10 tests covering session/new, session/load,
session/load not-found, set_model failure, ListModels, blocked args,
protocol verification (session/load not session/resume), timeout,
usage tracking, and backend construction
7. sidecar_manifest_test.go: Add qwenpaw to allFileBasedProviders
* fix: address Bohan-J second-round review on qwenpaw backend (PR multica-ai#5986)
Blocking fixes:
1. qwenpaw.go: Send qwenpaw.coding_project_dir inside _meta, not as
top-level parameter — ACP Pydantic model ignores extra root-level
keys (model_config has no extra='allow'), only merges field_meta
into kwargs
2. qwenpaw.go: Handle set_model returning result:null (not RPC error)
— real QwenPaw server swallows exceptions and returns None, which
serialises as JSON null; json.RawMessage('null') is non-nil so
bytes.Equal check is needed
3. context.go: Workspace skills are at <workDir>/skills, not
<workDir>/.qwenpaw/skills (confirmed against QwenPaw v2.0.1)
4. local_skills.go: QwenPaw resolves global root from
QWENPAW_WORKING_DIR -> COPAW_WORKING_DIR -> ~/.copaw -> ~/.qwenpaw,
not from QWENPAW_HOME
Test additions:
5. TestQwenpawSetModelReturnsNull — regression test for result:null
6. TestQwenpawSessionLoadTransientError — transient error does not
set ResumeRejected=true
7. TestQwenpawSessionNewSendsCodingProjectDir — verifies _meta format
8. TestQwenpawSessionLoadSendsCodingProjectDir — same for session/load
9. TestQwenpawTimeout — deterministic sync via signal file
All 15 Qwenpaw tests pass. Verified against real qwenpaw v2.0.1 binary
(end-to-end: prompt, session ID, resume, system prompt).
* fix: rebase against upstream/main and add per-task qwenpaw workspace/agent isolation
- Rebase add-qwenpaw-backend branch on upstream/main (88 commits ahead)
- Resolve conflict in config.go: keep refactored probe() with shell
resolution fallback, which already includes qwenpaw probe
- Add --workspace and --agent CLI args to qwenpaw acp for per-task
skill isolation and agent identity isolation
- Mark --workspace and --agent as blocked in qwenpawBlockedArgs so
user custom_args cannot override them
- Add deriveQwenpawAgentID() to produce deterministic agent IDs
from task's issue ID and agent ID
- Wire QwenpawWorkspace and QwenpawAgentID through ExecOptions
- Add prepareQwenpawWorkspace() in execenv to materialize bound
skills into a per-task workspace directory
* fix: address Bohan-J third-round review on qwenpaw backend (PR multica-ai#5986)
Three blocking issues resolved:
1. Agent ID registration — remove session/set_model and --agent entirely.
The simpler route: QwenPaw model override is declared unsupported,
eliminating the need for agent profile registration in QwenPaw config.
2. Skill revocation — prepareQwenpawWorkspace now does os.RemoveAll
on the skill_pool dir and manifest before rebuilding, making it
idempotent. A->empty (revoke all), A->B (replace), and A->A
(repeated reuse) all work correctly. Added 3 new unit tests.
3. Skill root path — changed 'skills' to 'skill_pool' in both
prepareQwenpawWorkspace and skillsDirPath to match QwenPaw's
store.py get_workspace_skills_dir.
Also cleaned up: removed deriveQwenpawAgentID function and its test,
removed QwenpawAgentID from ExecOptions, removed --agent from
qwenpawBlockedArgs, removed session/set_model test cases.
* fix: add missing qwenpaw probe() call in agents_probe.go
The TestDefaultAgentCommandNamesCoversAllProbes test found only 17
probe() calls in agents_probe.go but defaultAgentCommandNames has 18
entries. The probe() call for qwenpaw was missing, causing the
backend CI test failure.
Adding the probe ensures GUI-launched daemons can resolve qwenpaw
via the login shell fallback, matching all other providers.
* fix: sync models.go with upstream/main (remove qwenpaw from ListModels)
* fix: CI failures — migrate prefix 235→236 + update test
- migration prefix 235 was reused by upstream 235_chat_message_quick_actions;
renamed our qwenpaw migration from 235 to 236
- TestQwenpawListModels called len() on Catalog struct (compile error);
fixed to expect error for unknown provider type
* fix: bump qwenpaw migration prefix 236->241 (upstream took 236)
* fix: qwenpaw local skill root — 'skills' → 'skill_pool' (MUL-5355)
Bohan-J third-round review blocker #3: local_skills.go still scanned
<QWENPAW_HOME>/skills but the QwenPaw shared skill pool is
<QWENPAW_HOME>/skill_pool (store.py get_workspace_skills_dir).
Verified no other qwenpaw paths in the codebase assume the wrong layout.
* fix: bump qwenpaw migration prefix 241->242 (upstream took 241)
lint test fails with: migration prefix 241 is reused by
[241_comment_parent_lookup_index 241_runtime_profile_add_qwenpaw].
Upstream added 241_comment_parent_lookup_index; bump our migration.
* feat: add QwenPaw integration test and version declaration (MUL-5355)
- New: server/pkg/agent/qwenpaw_integration_test.go with three
agentintegration build-tagged tests:
TestQwenpawRealACPSmoke — full end-to-end ACP smoke test with
session/new → session/prompt and session/load resume validation.
TestQwenpawRealWorkspaceSmoke — validates skill_pool workspace
flag handling and per-task skill isolation.
validateQwenpawVersion — attempt version detection via qwenpaw
--version, pip show qwenpaw, and python import.
- Document QwenPaw v2.0.1 as the supported baseline version in
qwenpaw.go package comment, noting the contract details:
_meta qwenpaw.coding_project_dir for Coding Mode,
session/set_model NOT supported,
skill_pool workspace layout.
- This test suite is gated by MULTICA_RUN_REAL_AGENT_SMOKE=1 and
requires qwenpaw on PATH, matching the pattern used by grok,
cursor, and traeecli integration tests.
* fix: bump qwenpaw migration prefix 242->243 (upstream took 242 for qoderclicn)
Upstream added 242_runtime_profile_add_qoderclicn in the same rebase
window, colliding with our 242_runtime_profile_add_qwenpaw. The
migration lint test TestMigrationNumericPrefixesStayUniqueAfterLegacySet
catches duplicate prefixes after the legacy range.
Also add 'qoderclicn' to our migration's CHECK constraint so it doesn't
regress the whitelist added by upstream's 242.
* temp: stub out integration test to isolate CI failure
* fix: restore upstream probeAgentCLIs() call in config.go (rebase regression)
Rebase conflict resolution accidentally reverted upstream's MUL-5439
refactor (extracting probe logic to agents_probe.go) back to the old
inline probe block. This also dropped qoderclicn detection and
duplicated the probe logic already in agents_probe.go.
Restore the single 'agents := probeAgentCLIs()' call — qwenpaw is
already probed in agents_probe.go.
* fix: bump qwenpaw migration prefix 243->251 (upstream took 243-250)
Upstream added migrations 243-250 since our last rebase. Bump to 251,
the next available prefix.
* feat: add QwenPaw integration test (agentintegration build tag)
TestQwenpawRealACPSmoke drives the real qwenpaw acp binary end-to-end:
- session/new + session/prompt produces 'pong'
- session/load resume with ResumeSessionID works
- --workspace flag is forwarded correctly
Gated by MULTICA_RUN_REAL_AGENT_SMOKE=1, matching grok/cursor pattern.
Validated against QwenPaw v2.0.1.
* fix: workspace skills dir 'skills' not 'skill_pool' + add skill loading integration test
Two fixes:
1. qwenpaw_workspace.go: write skills to <workspace>/skills/ instead of
<workspace>/skill_pool/. QwenPaw's get_workspace_skills_dir() looks
for workspace skills at <workspace>/skills/ (store.py:65-67), not
skill_pool (which is the shared pool at WORKING_DIR/skill_pool).
Verified against real qwenpaw acp — skills in skill_pool/ were never
discovered.
2. Add TestQwenpawRealWorkspaceSkill integration test that proves a
bound skill is actually loaded and effective: writes a skill that
overrides the agent's response, sends an unrelated prompt, and
asserts the skill's marker text appears in the output. This
addresses R3 review feedback: 'please also add a test that exercises
an actually-bound skill'.
All three integration tests pass against QwenPaw v2.0.1:
- TestQwenpawRealACPSmoke (session/new + prompt + session/load resume)
- TestQwenpawRealWorkspaceSkill (skill discovery + effectiveness)
* feat: add ACP model discovery for qwenpaw via session/new models field
QwenPaw v2.0.1+ now includes a 'models' field (SessionModelState) in
the session/new response, added by agentscope-ai/QwenPaw#6531. This
lets ACP clients discover available models without session/set_model.
- ListModels for qwenpaw now uses discoverACPModels (same pattern as
traecli/grok/kiro) to spin up 'qwenpaw acp', call session/new, and
parse the models catalog from the response.
- discoverQwenpawModels mirrors discoverTraecliModels — ACP-native,
no auth selection needed.
- Model override via session/set_model remains unsupported: it
persists to agent.json at the agent scope (not session-scoped),
so calling it would mutate the user's shared agent config. The
model picker shows available models for display/selection, but
the daemon does not send set_model.
- Updated TestQwenpawListModels to verify qwenpaw is a recognized
type (not 'unknown agent type' error).
* fix: address Bohan-J Review 5 — ModelSelectionSupported=false, version bump to v2.1.0-beta.1, remove debug files
- ModelSelectionSupported('qwenpaw') now returns false with rationale
(session/set_model persists to agent scope, not session scope)
- Add TestQwenpawModelSelectionUnsupported regression test
- Update version references from v2.0.1 to v2.1.0-beta.1 (includes
agentscope-ai/QwenPaw#6531 — models field in session/new response)
- Remove check_ci.py, jobs.json, runs.json debug artifacts
* fix: bump qwenpaw migration prefix 251->253 (upstream took 251)
Upstream added 251_agent_runtime_unbind. Bump to 253, the next
available prefix after 252_agent_builder_draft.
* fix: address Bohan-J Review 6 — drop unused discovery, always attribute to unknown
- ListModels for qwenpaw returns empty catalog without spawning ACP
subprocess (model selection is unsupported, so no consumer exists)
- Usage attribution always uses 'unknown' instead of opts.Model
(the backend never sends opts.Model to QwenPaw)
- Add TestQwenpawUsageModelIgnored regression test
- Fix stale v2.0.1 comment in TestQwenpawListModels
* chore(agent): clean up qwenpaw model-discovery leftovers
Follow-up nits from review 7 on PR multica-ai#5986:
- Drop discoverQwenpawModels: it lost its only caller when ListModels
started returning an empty catalog for qwenpaw.
- Correct the version contract in qwenpaw.go. The execution path needs
only the ACP surface present in v2.0.1 (current stable); the models
field on session/new landed in v2.1.0-beta.1 but has no consumer now
that model selection is unsupported.
- Make TestQwenpawListModels actually guard the no-subprocess promise.
It pointed at a nonexistent path, which the old discovery helper also
answered with an empty catalog, so it passed either way. It now uses
an executable fake that records invocation; verified it fails if a
discovery path is reintroduced.
- gofmt agent.go (ExecOptions alignment broke when QwenpawWorkspace was
added) and restore the trailing newline in qwenpaw_test.go.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: niudakok <niudakok@users.noreply.github.com>
Co-authored-by: Bohan-J <bohan.optimism@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>
0xHexE pushed a commit that referenced this pull request Aug 21, 2026
* 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 multica-ai#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 <github@multica.ai>
---------
Co-authored-by: Sol-Boy <sol-boy@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@0xHexE@NevilleQingNY@multica-eve@tangyuanjc@Bohan-J@etern@vicksiyi