* fix(lark): resolve outbound references to workspace links
* test(wecom): use normal budget for no-holder state assertion
* fix(channel): preserve Markdown reference source boundaries
* fix(lark): format references with one workspace lookup
* fix(deps): update x/net for HTML parser advisory
---------
Co-authored-by: Frezc <[email protected]>
A merged PR moved its issue to the workspace's pr_merge_status even when the
issue's project workflow does not list that status, bypassing the check a
manual status change goes through. The decision now loads the project
workflow and returns not_in_workflow instead; the issue page renders nothing
for it, so the issue silently stays where it is. Default workflows and issues
outside any project are unchanged.
Co-authored-by: Multica Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* MUL-8292: refactor(brief): move Available Commands detail behind --help pointers
Each bullet keeps its signature and the behaviour needed at the call site;
wakeup condition catalogue, PR snapshot mechanics, actor filters and the
repeated file rules now live only in --help and the multica-platform
references. Issue brief 17,300 -> 16,372 bytes (-928); chat and autopilot
briefs drop by the same amount. New TestAvailableCommandsKeepsCompressedContracts
pins the kept contracts and guards the moved detail from drifting back.
Co-authored-by: multica-agent <[email protected]>
* MUL-8292: fix(brief): keep Available Commands rules self-contained in every kind
Review follow-up on #9596:
- comment list / issue create / comment add pointed at Workflow step 2 and
Comment Formatting, which chat and autopilot briefs do not have; the
bounded-read recipe and the workdir file rule are inline again.
- repo checkout restores both keep conditions and the clean-checkout case.
- wakeup scopes last_error to missing PR links/snapshots; a pending check or
unmet condition records nothing.
Contract test now runs every kind x provider, and a new test fails when the
shared section references a heading or workflow step the kind lacks.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
* fix(runtimes): spell out Qoder Cloud Agents instead of QCA
The UI, docs, landing copy and server messages called the product "QCA"
in labels such as "QCA Agent", "QCA runtime" and API errors like "QCA
token is required". Use the full name Qoder Cloud Agents everywhere a
person reads it, and add it to the brand list in the naming conventions
so the abbreviation does not come back.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix(runtimes): refine Qoder Cloud Agents copy (MUL-8282)
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: 安辰 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(runtimes): price Bedrock-served Anthropic ids and DeepSeek-V4.1-Flash
Bedrock reports Anthropic models as `<region>.anthropic.<id>` with an
inference-profile version on snapshots (`us.anthropic.claude-sonnet-5`,
`eu.anthropic.claude-sonnet-4-5-20250929-v1:0`). Neither resolver accepted
that shape: the backend alias rules anchor on `/`, `:` or start-of-string,
and the frontend `canonicalCandidates` only strips `/` and `:` routing
prefixes, so the vendor and region prefixes survived into the lookup. Every
Bedrock-served turn therefore missed the rate table and rendered as $0.00
plus an "unmapped models" warning, while the tokens were still counted in
`task_usage` (cost_usd_ticks is provider-reported and NULL for Anthropic, so
nothing else was pricing them either).
Strip the Bedrock vendor/region prefix and the `-v<digit>:<digit>` profile
version on both sides, before the existing dot↔dash normalisation. Only
`anthropic.`-prefixed ids are rewritten, so non-Anthropic Bedrock models
(`us.amazon.nova-*`) stay unmapped rather than borrowing a Claude rate.
DeepSeek-V4.1-Flash is also missing: the catalog lists v4-flash and v4-pro
only, and there is deliberately no family fallback, so `deepseek-v4.1-flash`
and OpenRouter's dated `deepseek-v4.1-flash-1017` both went unpriced. Add the
official `deepseek-flash` sheet rate ($0.15/$0.60, cache read $0.003, cache
write mirrors input) and one alias rule that admits the dated snapshot —
DeepSeek documents that id as the model actually served for the undated slug,
so it is the same SKU at the same rate, not a second tier.
Both tables are updated together (server/internal/metrics/pricing.go drives
the Prometheus counters, packages/views/runtimes/utils.ts drives the
dashboard) and both suites pin the new ids, including the negative cases: a
later SKU, a `-preview` qualifier or a non-Anthropic Bedrock id must stay
unmapped.
* ci: re-run (previous backend-tests failure was in channel-media reconciler timing tests, unrelated)
* fix(runtimes): align Flash pricing and constrain Bedrock aliases (MUL-8269)
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(wakeups): accept blocked in --until-issue-state
Lets a coordinator wait for a specific child to reach blocked, alongside the
existing in_review handoff, without waiting for the child to close.
Exact status key match, same as in_review.
Refs #9344
Co-authored-by: multica-agent <[email protected]>
* fix(i18n): keep blocked state key in lowercase English in zh wakeup copy
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
* fix(execenv): let a re-dispatched task fork past its own busy-branch fallback (#9198)
When the conversation branch is held by another worktree, a local_directory worktree task forks onto <branch>-<task key>. That name is deterministic, so the same task re-dispatched while the branch is still busy hit 'a branch named ... already exists' and failed every attempt. Fork a fresh timestamped branch instead, leaving the earlier attempt's branch (and any partial work on it) untouched.
Co-authored-by: multica-agent <[email protected]>
* fix(execenv): keep picking an unused fallback branch name on repeated re-dispatch
A second-granular suffix still collides when the same task is prepared more than twice within one second. Try a bounded sequence of fallback names (<alt>, <alt>-<unix>, <alt>-<unix>-N) instead of failing on the first collision; earlier attempts' branches stay untouched. Addresses review on #9547.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflow): brief a handler-less step as the assignee's span to the next handoff (MUL-8231)
Co-authored-by: multica-agent <[email protected]>
* fix(workflow): leave done to a human from an unconfigured in_review (MUL-8231)
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
UpdateProjectResource derives every column it writes back from the row
read at the top of the request, but the read and the write ran outside
any transaction. Two concurrent partial updates could interleave as
read-read-write-write, and the second writer's stale snapshot silently
reverted the first writer's change: a label-only PUT reverted a racing
ref update to an old Git ref, and a label-only PUT on a local_directory
restored a stale execution mode (reported in #9238 with a deterministic
interleaving that fails 15/15 under -race -count=3).
Serialize the flow with SELECT ... FOR UPDATE inside a short transaction
(GetProjectResourceInWorkspaceForUpdate + Begin/WithTx, same shape as
CreateAgent). Competing updates block on the row until the write lands,
then re-read fresh. This keeps the local_directory legacy label-mirror
writeback intact, which a presence-aware column rewrite alone would have
broken for mirror-triggered ref updates.
Review follow-ups:
- The regression test no longer leaves the race to the scheduler. Before
resuming the paused label request, it waits until the ref request has
either completed outright (the only exit on the pre-fix code, which
never blocks) or is blocked waiting for the label request's row lock
(the only exit with the fix, seen as wait_event_type='Lock' on the
backend running the FOR UPDATE query, checked via pg_stat_activity).
The two signals are mutually exclusive, so the test fails
deterministically without the lock and passes with it.
- findLocalDirectoryConflict and requireWorktreeCapableDaemon now read
through caller-provided queries instead of the handler's pool: on the
update path they run inside the transaction, so a locked request no
longer borrows a second pooled connection per request.
- Added a local_directory concurrency case: a label-only update racing
an execution_mode update through the label-mirror ref rewrite.
The regression tests fail on the pre-fix code ("stored ref = main, want
the concurrent ref update (release)" / "stored execution_mode = worktree,
want the concurrent mode update (cleared = in_place)") and pass with the
lock, 20/20 runs each way.
Fixes#9238
Generated-by: GLM-5.3-Flash (ZCode)
* fix(issues): stop repeating "wake" in a time wakeup's timeline entry
A time rule's label already reads "wake at ..." (or "every hour", "每小时唤醒"),
so the created entry said "…09:00 唤醒唤醒 Agent" in Chinese and
"Wake at 09:00 · wake Agent" elsewhere. Time rules now use a sentence that
names the agent first: "为 Agent 添加了唤醒:…09:00 唤醒".
* refactor(daemon): remove the unused MULTICA_KEEP_ENV_AFTER_TASK setting
Its only reader went away when run directories stopped being removed at the
end of a run so later runs on the same issue can reuse them (ce2b263ea).
Retention is controlled by the MULTICA_GC_* settings.
* feat(agent): add host-local Devin CLI over ACP
Spawn `devin acp` via the shared ACP client. Local CLI only — not
cloud Devin VMs/Playbooks/Secrets. Model via `devin acp --model`.
Catalog from `devin models list --format json` (families/variants).
* fix: skip Devin ACP handshake when browser auth is also offered
* fix: close dim case string so Devin compiles
Missing quote after dim broke runtime_config.go and every Go CI job.
* fix: stop runtimeConfigPath recursion on Devin
Devin is a builtin whose ProtocolFamily is itself. Recursing on that
overflowed the stack and killed backend-tests.
* test(agent): cover Devin model failures and final-answer delivery
Co-authored-by: multica-agent <[email protected]>
* fix(agent): honor Devin model aliases and stored login (MUL-6623)
Co-authored-by: multica-agent <[email protected]>
* fix(agent): drain Kiro panic details before terminating runtime (MUL-6623)
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(i18n): add zh-Hant locale to web and desktop
Add Traditional Chinese (zh-Hant) as a supported locale with a full
translation of the shared views bundles. Browser tags zh-TW/zh-HK/
zh-Hant now resolve to zh-Hant instead of collapsing to zh-Hans; the
document lang is zh-TW, and docs/use-case links reuse the /zh pages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit a6b3f6ba53a605c3e06fd6c4d30fd9df6fca8645)
* feat(mobile): add zh-Hant locale
Ship Traditional Chinese bundles for mobile and resolve zh-Hant/TW/HK/MO
device locales to them instead of falling back to English.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 04563c22662bdb735a9a6c04be784abd9f87da06)
* feat(ui-lab): add zh-Hant locale
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 1351ea572ed5d885d8a29034d6c0317de58641ea)
* feat(server): accept zh-Hant as user language
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 50dfe77a4c45452fc877c62497aba226d5aaa426)
* fix(i18n): sync zh-Hant with upstream copy changes
Upstream added conversation-starter, instruction-preview and board column
strings, removed restated descriptions (MUL-8162) and renamed Qoder Cloud
Agents. Mirror those key additions, removals and rewordings in zh-Hant so
the locale parity test passes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 3a97dfc7e0bbf7568925f735c00fc69b6d94313d)
* fix(i18n): add zh-Hant copy for member-scoped runtime visibility
Mirror the runtime visibility "members" choice and the reworded revoke
dialog in zh-Hant so the locale parity test keeps passing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix(i18n): replace Mainland terms in zh-Hant copy with Taiwan usage
Several zh-Hant strings were converted character by character from
zh-Hans and kept Mainland vocabulary, which reads wrong to Traditional
Chinese users: 郵箱 → 信箱, 賬單/賬號 → 帳單/帳號, and similar. Values
only; no keys added or removed.
* fix(i18n): polish zh-Hant copy from review
- 分割槽 → 分區 and 許可權 → 權限 (OpenCC phrase mis-conversions)
- drop stray blank lines before zh-Hant workspace names
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Multica Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(lark): deliver reply images and files
Based on #8332 by @zprial, addressing its review of bounded delivery,
resource uploads, uncertain sends, file fallback and HTTP boundary tests.
Render bound images with the reply text in one card, pair video/audio
uploads with their message types, preserve pending-send uncertainty and
normalize Markdown destinations before matching bound attachments.
Keep runtime artifact ingestion and deployment-specific configuration
outside this change. Add transport, ownership and rendering regressions
and document the behavior in all supported languages.
* refactor(lark): scope attachment transfer slots to the patcher
The concurrency semaphore was a package-level global while the admission
and pending tiers lived on each Patcher. Move it onto the Patcher so all
three limits share one scope and tests no longer mutate global state.
MUL-8205
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): defer issue status rules to the project workflow (MUL-8145)
When the claim carries a project workflow, the runtime brief no longer
tells the agent to set in_progress first and in_review on delivery. Those
statuses may be outside the workflow (the server rejects them), and
entering a handoff step reassigns the issue. The brief now points at the
per-turn [Project workflow] block, and the workflow brief carries the
entry/exit checks in workflow terms. Briefs without a workflow are
byte-identical to before.
Co-authored-by: multica-agent <[email protected]>
* fix(squad): word the owning leader's status moves by the project workflow (MUL-8145)
The squad protocol (Agent Identity, which outranks the runtime brief) still
told an owning leader to move the parent to in_progress and then in_review,
which a workflow may not list and which can hand the parent to another
step's handler. When the claim carries a project workflow, responsibility 6
now keeps the ownership grant but moves the parent only through the
[Project workflow] commands. The claim resolves the workflow before the
briefing to choose the wording. Guest leaders and issues without a
workflow are unchanged.
Co-authored-by: multica-agent <[email protected]>
* refactor(daemon): list no status keys in the brief for a workflow issue (MUL-8145)
With a project workflow, [Project workflow] already lists the issue's
statuses and the commands that move it, and the built-in keys may not be
accepted there. The status command line now keeps only the command and
points at that block; other issues learn valid statuses from the error.
Co-authored-by: multica-agent <[email protected]>
* refactor(workflow): stop forbidding status moves nothing asks for (MUL-8145)
Under a project workflow the current runtime brief no longer states the
built-in in_progress / in_review moves, so the lines that forbade them
(in [Project workflow], the runtime brief and the squad protocol) only
named statuses the agent had no reason to use. Drop them, and the exit
check the runtime brief already states.
Older daemons still state the built-in rules, so the daemon now
advertises workflow-status-rules-v1; without it the claim keeps the
override lines in [Project workflow] and the squad protocol. The
multica-platform skill notes that [Project workflow] supersedes its
built-in status arc.
Co-authored-by: multica-agent <[email protected]>
* refactor(workflow): shorten the step's moves in the workflow brief (MUL-8145)
Replace the handoff preamble with a plain "Your moves:" heading, name
who each move hands off to on its own line, and drop the "this
workflow has no blocked status" explanations from Can't proceed.
Co-authored-by: multica-agent <[email protected]>
* refactor(workflow): state each step outcome as the status to set (MUL-8145)
The moves block read like raw CLI ('Step done → multica issue status MUL-7 x').
Each outcome now says which status to set and that the server assigns the
next handler; the CLI form is given once.
Co-authored-by: multica-agent <[email protected]>
* Revert "refactor(workflow): state each step outcome as the status to set (MUL-8145)"
This reverts commit 2d82d2d342a99afe88cc40c48516b7ea8e860d1e.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Preserve workflow-selected assignees when description updates refresh untouched nullable fields under the issue row lock. Apply the same behavior to single and batch updates while retaining explicit-assignee precedence.
Add regression coverage for single and batch handoffs, unchanged-status retries, and explicit member assignments. All applicable PR CI checks passed on 3c5f8d4.
Co-authored-by: Multica Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Use one slash-delimited path contract for skill authoring endpoints and daemon bundle validation. Cover canonical path handling and all public file-write routes with regression tests.
* fix(openclaw): carry per-run model override on the run input (INFRA-71)
Reimplement MUL-7376 (#8419) on this fork and address the review points.
Resolve runtime_config.model_override once, at claim time, and carry it on the run input (model_override); the daemon no longer decodes it from mutable agent config.
OpenClaw keeps agent.model as the registered agent id (--agent) and the override as the underlying model (--model); arbitrary --model from custom_args stays blocked. Usage attribution falls back runtime model -> per-run override -> unknown, never the agent id.
Round-trip model_override through the runtime-config schema, serializer, equality check and the runtime-config tab (i18n in all locales). Docs updated.
Co-authored-by: multica-agent <[email protected]>
* docs(openclaw): clarify persistent agent model overrides
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Multica Eve <[email protected]>
* fix(daemon): tell resumed runs when their trigger comment was edited (MUL-8218)
A comment edit cancels the affected run and retriggers from the edited
comment, but the follow-up resumes the cancelled run's session, whose
history still quotes the pre-edit text (GH #9492).
The claim now sets trigger_comment_edited_at / trigger_comment_revision
when the trigger was created before the resumed run started and edited
after it. The daemon renders an [EDITED COMMENT] header plus a note that
the quoted text is the current version, while work already done in the
session still happened.
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): keep edit flag after resolve and for coalesced comments (MUL-8218)
Review of #9526 found two paths that resumed a session holding the old
text without flagging the edit:
- A resolve after the edit moved updated_at to resolved_at and the
resolve exclusion hid the edit. Only a single resolve on a first-
revision row (revision 2, resolved_at == updated_at) is now treated
as resolve-only; anything else errs toward the note.
- An edited comment folded behind a newer, unchanged trigger lost its
flag because only the trigger was checked. Every delivered coalesced
comment now carries edited_at / revision, and the prompt marks it
EDITED with the shared current-version guidance.
Adds the reviewer's two regressions.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
* fix(skills): preserve complete bundles when source fetching fails
* fix(skills): skip submodules in the contents-crawl fallback
The contents API lists submodules inside a directory as type "file" with
a null download_url. The crawl fallback rejected them as a missing
download URL, so a skill containing a submodule failed with 503 whenever
the repository tree was truncated, while the tree path skips them. Detect
submodules via their git_url tree pointer and skip them; a regular file
without a download URL still fails the import.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Multica Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(autopilot): settle create_issue runs by first task outcome, not issue blocked (MUL-8200)
Co-authored-by: multica-agent <[email protected]>
* fix(autopilot): settle create_issue runs only from their own dispatch chain (MUL-8200 review)
Co-authored-by: multica-agent <[email protected]>
* fix(autopilot): root create_issue dispatch chain at the recorded task and let its outcome win (MUL-8200 re-review)
Co-authored-by: multica-agent <[email protected]>
* fix(autopilot): resolve unlinked create_issue dispatch by agent and wait for pending enqueue (MUL-8200 review)
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
* MUL-8177: fix(daemon): label worktree finalization failures as local_directory_error (#9447)
Co-authored-by: multica-agent <[email protected]>
* test(wecom): read quoted-bind body and attachment count from one snapshot
The poll read the message body and then counted its attachments in a
second statement. When the bind transaction committed between the two,
the count said binding was done while the body still held the unbound
"[Image: unavailable]" markers, failing TestAQuotedPictureIsNamedInTheStoredBody
intermittently in CI.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(agent): preserve runtime config boundaries after formatting
Keep original trailing newlines inside the managed block so formatter normalization does not leak cleanup changes into task commits.
Constraint: Preserve user edits outside the managed brief and legacy block behavior.
Rejected: Restore the entire original file | Discards agent-authored changes.
Confidence: high
Scope-risk: narrow
Tested: execenv suite with race and shuffle; go vet; Prettier 3.6.2 and Oxfmt 0.40.0 file round trips.
Not-tested: Full daemon auto-finalize integration.
* fix(agent): don't restore boundary into replaced body; keep CRLF consistent
Follow-up to the formatter-boundary fix (MUL-8114, review on #9382):
- Record whether the pre-existing file was blank in the boundary metadata.
An empty prefix at cleanup only means "blank original" when that flag is
set; otherwise the agent removed or moved the original content, and
re-adding the old trailing newline produced "\n# New\n".
- Treat any two line breaks (LF or CRLF) as the collapsed separator and
restore the recorded boundary in the formatter's line-ending style, so
CRLF files no longer end up mixed or with extra CRLFs.
- locateMarkerBlock consumes a CRLF after the end marker.
- Restore the removed test doc comment and update the inject/cleanup docs
to describe the metadata line.
Co-authored-by: multica-agent <[email protected]>
* fix(agent): don't restore blank-file boundary in front of new content
A newline-only CLAUDE.md that the formatter stripped and the agent then
wrote a body into (after the managed block) got the recorded newline
restored in front of the new body, failing prettier --check. Only restore
a blank file's boundary when nothing follows the block (MUL-8114).
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: shaun0927 <[email protected]>
Co-authored-by: Multica Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(kiro): fail fast when the runtime panics mid-turn (MUL-8188)
Kiro can panic inside its agent loop while the ACP process stays alive, so
session/prompt never answered and the task hung until the 2h idle watchdog
or a manual cancel. Watch stderr live for a panic header, abort the prompt,
and surface the panic text. A resumed session that panics before its first
message signals ResumeRejected so the daemon retries once on a fresh session.
Co-authored-by: multica-agent <[email protected]>
* fix(resume): stop re-resuming sessions the runtime cannot drive (MUL-8188)
- taskfailure.UnresumableHistory matches "invalid conversation history",
so the daemon's fresh-session retry, classifyPoisonedError and
ResumeUnsafeFailure all treat Kiro's panic as resume-unsafe; the
GetLastTaskSession / GetLastChatTaskSession text guards mirror it for
rows written by older daemons.
- A resumed run cancelled after >=5 minutes with zero agent output now
retires its session: the daemon sends retired_session_id on cancel-ack
and the server records it on the cancelled row (status CAS, never
overwrites) and clears a chat pointer still naming it.
Co-authored-by: multica-agent <[email protected]>
* fix(resume): address review on retired sessions (MUL-8188)
- Manual retry no longer revives a retired session: the exact-source rerun
claim path now checks IsTaskSessionRetired (fails closed), still reuses
the source workdir, and sets PriorSessionResumeUnavailable.
- Silent-resume retirement measures awakeclock time, so host suspend
(Windows monotonic clock counts through sleep) cannot retire a healthy
session; adds a suspend regression test.
- Handler tests use dbfx.Issue/Task and testutil.Call per AGENTS.md.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
* feat(server): record structured claim decision outcomes
Every daemon poll decides whether a task is dispatched, skipped, or left
waiting, but most of those decisions were invisible: claimTask's outcome
string only surfaced through the >=300ms slow log, two outcomes had
scattered debug lines, and no_runtime / runtime_mismatch had nothing at
all. The worst blind spot is no_tasks, which collapses ClaimAgentTask's
SQL fences (wakeup gate, runtime liveness, per-(issue, agent)
serialization, rebind) into one value even though the caller's candidate
list knew whether work was visible (#8551).
Add a single observability funnel, recordClaimDecision, called once per
claim attempt from claimTask's defer and once per claim-poll exit that
never reaches claimTask. It emits one bounded-cardinality counter
(multica_agent_task_claim_decision_total{source, outcome, reason}) and a
structured debug log. The reason split inside no_tasks —
candidates_blocked vs no_candidates — attributes a queue wait to a fence
rather than to an empty queue. Idle-poll outcomes are counted but not
logged to keep the hot path quiet. No decision logic changes: outcomes
mirror strings the claim paths already produce, and the caller-supplied
claimScope is informational only.
Co-Authored-By: Claude Code <[email protected]>
* feat(server): cover batch claim polls and sharpen decision reasons
Address review feedback on the claim decision funnel:
1. Batch poll decisions now reach the counter: ClaimTasksForRuntimes
records its promote/reclaim/list error exits, its empty-cache
short-circuit, its zero-candidate result, and each recovered
stale-dispatched task. The counting unit is documented in the funnel:
one increment per claim attempt plus one per poll-level decision,
which are distinct outcomes and cannot double-count.
2. Reason labels now state only what the evidence supports. The
candidate queries already filter wakeup validity and agent/runtime
binding, so an empty result is no_visible_candidates, never
empty_queue. Candidates observed with no_tasks is
candidates_unclaimed — the exact cause (SQL fence vs lost race)
is unknown from the Go side.
3. The source label is renamed to claim_path so it cannot be mixed with
the task-origin source label of the other multica_agent_task_*
metrics in shared dashboard filters.
Adds batch-path DB coverage (claimed, no visible candidates,
candidates unclaimed) alongside the existing singular-path cases.
Co-Authored-By: Claude Code <[email protected]>
---------
Co-authored-by: Thneoly <[email protected]>
Co-authored-by: Claude Code <[email protected]>
* fix(daemon): let a worktree turn repair a delivery it moved off its branch (MUL-8139)
Worktree-mode turns now get told which branch and starting commit their
delivery must stay on. When a turn still ends with its work squashed past
the baseline or committed off the task branch, the daemon runs one repair
turn asking the agent to put the work back before Finalize decides.
The MUL-6881 delivery guard is unchanged; resets past the baseline and
repairs that would revert the baseline's content are never offered.
Refs #9404
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): refuse repairs that would revert baseline edits; no supplements in repair turn (MUL-8139)
Review follow-up on the worktree delivery self-repair:
- repairWouldRevertBase now proves at the content level that the agent's
files already carry every change the baseline made relative to its merge
base with tip (equal content, or a clean three-way merge that leaves the
agent's file unchanged). A path-level "both sides touched it" check let a
repair silently revert a user's edit in a file the agent also changed.
Deletions, non-regular or binary files, conflicts and git errors refuse.
- The repair turn no longer accepts additional messages: its reply is
discarded, so an accepted message would be acked as delivered and its
answer lost. Unclaimed, it settles as turn_ended and the receipt says so.
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): keep file modes in the worktree repair preservation check (MUL-8139)
repairWouldRevertBase compared only text, so a user's chmod +x recorded in
the baseline was lost when the agent worked from an older checkout: the
repair re-committed run.sh as 100644. The agent's side is now read from the
tree `git add -A` would stage (built in a private copy of the worktree
index), which is exactly what RepairSteps commits, and every path the
baseline changed must keep the baseline's mode. A mismatch, or an index that
cannot be copied, refuses the repair.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat: share runtimes with selected workspace members
Co-authored-by: multica-agent <[email protected]>
* refactor(runtimes): one-click private/public and popover member picker
Private and Public apply immediately again (restrictions still go through
the reviewed revocation dialog). Specific members opens a draft popover
with search, Cancel and Save; selected recipients are summarized under
the scope control with an Edit entry point. The editor moves to its own
runtime-access-editor component.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
* feat(telemetry): report visiting and delegating member counts in self-host snapshot v2 (MUL-8161)
Co-authored-by: multica-agent <[email protected]>
* fix(telemetry): count delegating members from a human-action ledger, drop v1 fallback (MUL-8161)
- member_delegation_day records (member, UTC day) at the human action itself,
so retries, system wakeups and creator-fallback runs never count, and a
comment folded into a queued run keeps both humans and their own days.
- Activity read failures now fail collection and use the existing retry
instead of silently sending a v1 snapshot.
Co-authored-by: multica-agent <[email protected]>
* fix(telemetry): record create-with-assignee, honor DO_NOT_TRACK, build ledger PK concurrently (MUL-8161)
- A member creating an issue assigned to an agent or squad now records a
delegation day, on both the immediate and deferred create paths.
- TaskService.RecordMemberActivity gates the ledger and is set from the
startup telemetry config, so DO_NOT_TRACK stops collection too.
- member_delegation_day's primary key is built with CREATE UNIQUE INDEX
CONCURRENTLY + PRIMARY KEY USING INDEX (589-591), with cleanup registered.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): enforce runtime ownership on daemon routes (MUL-8154)
/api/daemon/* accepts any workspace member's PAT/JWT, and authorization only
checked workspace membership. Any member could claim another member's tasks
(receiving the agent's custom_env, MCP config and an owner-scoped task token),
write task results/messages, and take a runtime over by re-registering its
daemon_id.
- Runtime-level check on every runtime/task daemon route, HTTP heartbeat,
deregister, batch claim (and so the WS claim RPC) and the WS handshake:
user credentials must own the runtime (NULL owner fails closed); daemon
tokens must match a non-empty runtime daemon_id.
- Register: pre-check that every existing row on the daemon_id belongs to the
caller, plus a required_owner_id fence in both upserts (incl. the
failed-profile path) so a concurrent registration cannot win the row.
Daemon-token registration keeps the existing owner.
- Legacy merge: only for user-credential registrations, and only when the
caller owns both rows, verified on the FOR UPDATE-locked rows.
- Tasks without a runtime are not writable through daemon routes; status
stays readable.
- GetTaskStatus / WS lease queries carry runtime owner/daemon so the hot
paths keep one round trip.
Co-authored-by: multica-agent <[email protected]>
* test(daemon): authenticate fixtures as the runtime owner (MUL-8154)
Most handler tests drove daemon routes with a daemon token whose daemon_id
did not match the fixture runtime, relying on the workspace-wide token scope
this fix removes. They now authenticate the way real daemons do, with the
runtime owner's user credential (newOwnerDaemonRequest). Tests about daemon
token scoping keep mdt_ and get fixtures with a matching daemon_id; the
ownerless-runtime tests also assert that a user credential is refused.
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): serialize registration per daemon and keep Qoder WS claims working (MUL-8154)
Review of #9423:
- P1: the owner pre-check and the per-provider/profile upserts hit different
conflict keys, so two members registering different providers of the same
daemon could both pass the pre-check and both insert, leaving one daemon
with two owners (and a daemon token for either runtime reaching the
other's). Registration now runs the pre-check and every runtime write in one
transaction under a pg_advisory_xact_lock on (workspace_id, daemon_id), on a
single connection so queued registrations cannot starve the holder. Legacy
merges, analytics and the register event run after commit; failed-profile
records use savepoints so they stay best-effort.
- P2: the Qoder revocation check read the Authorization header, which the WS
RPC bridge's in-process request does not carry, so valid Qoder WS claims
silently returned nothing. DaemonAuth now records the mdt_ token hash in the
request context, the WS identity keeps it, the RPC bridge re-attaches it,
and the check validates that hash on every call (revocation still applies
to an open connection).
- Regression tests for cross-provider and provider+profile concurrent
registration, and for valid/revoked Qoder credentials over WS RPC.
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): bind daemon tokens to the runtime owner they were issued for (MUL-8154)
Second review of #9423: a daemon_id can be re-registered by another member
after its runtimes are deleted, and a daemon token was bound only to
(workspace, daemon_id), so the previous owner's Remote MCP token reached the
new owner's runtime (heartbeat, claim, owner-scoped task token).
- daemon_token.owner_id (migration 588). Remote MCP tokens are bound to the
locked runtime owner inside FinalizeTaskClaim; live tokens are backfilled
only where their daemon has exactly one owner.
- DaemonAuth carries the owner binding (DB and cache; cache key bumped to v2
so pre-upgrade entries are never read), the WS identity keeps it and the
RPC bridge replays it.
- Daemon-token authorization now also requires token owner == runtime owner.
Qoder-managed tokens stay unbound (governed by cloud_agent_connection), and
user credentials can no longer register the reserved qoder-managed-* ids.
- Tests: the reuse lifecycle over HTTP, a new WS handshake and an already
open WS connection; the reserved daemon_id; the concurrency barrier now
hooks the registration transaction (TxStarter) so the interleaving is real.
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): owner-bind daemon-token registration; drop unsafe token backfill (MUL-8154)
Third review of #9423:
- Migration 588 no longer backfills daemon_token.owner_id from the daemon's
current owner: that owner may be a later re-registrant, which re-bound the
previous owner's live token to them. Existing non-Qoder tokens stay NULL and
are refused (24h Remote MCP credentials, re-issued on the next claim).
- owner_id is a plain UUID column, no foreign key / cascade (AGENTS.md).
- Register via a non-Qoder daemon token now uses the token's owner for the
in-transaction pre-check and the SQL owner fence, and refuses unbound
tokens. Qoder keeps its connection-based check; daemon tokens still never
trigger legacy merges.
- Tests: an old owner's bound token and an unbound legacy token can neither
register over nor heartbeat the replacement owner's runtime.
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): grant the Qoder registration exemption only with a live connection (MUL-8154)
Fourth review of #9423: register skipped the token owner binding for any
qoder-managed-* daemon token, but checked the Qoder connection only when the
body declared a qoder_cloud runtime. A pre-upgrade Remote MCP token under that
prefix (no owner, no connection) could register type=claude and rewrite
another member's runtime.
Register now verifies the token against a live cloud_agent_connection before
anything else whenever the token is in the Qoder namespace, regardless of the
declared runtimes, and restricts that namespace to the bridge's own shape: a
built-in qoder_cloud runtime, no custom profiles, no failed_profiles.
Test covers built-in, custom-profile, failed-profiles-only and qoder_cloud
bodies with an unconnected token, plus the connected token's allowed and
refused shapes.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(auth): reject task-token requests that target a different workspace
A mat_ task token is bound to one workspace. Auth overwrote X-Workspace-ID
with that binding and both workspace resolvers short-circuit on task_token,
so a request that explicitly named workspace B (header, query, or slug)
silently ran against the token's workspace A.
Before the overwrite, Auth now checks every value of X-Workspace-ID,
X-Workspace-Slug, ?workspace_id and ?workspace_slug: a different workspace
returns 403, a malformed ID 400, an unknown slug 404, a slug lookup failure
500. No-selector and same-workspace requests, JWT/PAT behaviour and the
existing URL-param binding check are unchanged.
Fixes#9403
Co-authored-by: multica-agent <[email protected]>
* fix(auth): reject task-token requests with unparseable query strings
r.URL.Query() silently drops pairs it cannot decode, so a selector like
workspace_slug=ws-b%zz or workspace_id=<B>;x=1 was read as absent and the
request fell through to the token's workspace. Parse RawQuery strictly
with url.ParseQuery and return 400 before any handler runs.
Addresses review on #9422.
Co-authored-by: multica-agent <[email protected]>
* test(server): stop sending a conflicting workspace in source-context guard test
The test spoofed X-Workspace-ID to prove Auth overwrites it. Auth now
rejects a conflicting selector with 403 (#9403), which made the sibling
route return 403 instead of the downstream 404 the test asserts. Drop the
spoofed workspace so the 403s come only from the human-only route guard.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
* fix(runtimes): rename Qoder Cloud Agent to Qoder Cloud Agents
Qoder's product is called Qoder Cloud Agents, matching how Cursor Cloud
Agents is already spelled in the runtime lists. Update the display name,
default runtime name, error messages, translations, landing copy, docs
and tests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fix(runtimes): use Qoder's current logo
Replace the old green Qoder mark with the current monochrome mark from
qoder.com/favIcon.svg. The background is removed, the paths are
minified, and both paths use currentColor so the mark stays visible in
dark mode. It applies to the Qoder, Qoder CN and Qoder Cloud Agents
runtimes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: 安辰 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
An agent task token (mat_) authenticates as the runtime owner, and neither
POST /api/cli-token nor the /api/tokens group checked the actor source. A
prompt-injected agent could trade its 24h task token for a 30-day session
JWT or a non-expiring PAT, and could list or revoke the owner's PATs.
- Mount RequireHumanActor on /api/cli-token and the whole /api/tokens group
(also rejects mcn_ cloud-node PATs).
- Add a handler-level backstop in IssueCliToken and
CreatePersonalAccessToken so a future route refactor cannot drop the guard.
- Add router-level integration tests (real Auth -> router -> handler, with a
forged X-Actor-Source) and handler backstop tests.
Co-authored-by: multica-eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>