mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-10 18:36:45 +08:00
c8048a0c8a4e0528542b697fa5fcc67ea0c45e89
266
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
eb2a94e860 |
MUL-6368: Prevent cross-agent task and workdir contamination (#7205)
* fix(daemon): isolate task identity and workdir reuse Co-authored-by: multica-agent <[email protected]> * test(llm): use validated retry override in contract Co-authored-by: multica-agent <[email protected]> * fix(daemon): address task isolation review Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
728ba9c58e |
fix(daemon): stop concurrent tasks evicting each other's OpenClaw discovery cache (#7204)
loadOpenclawDiscoveryCache rejected any entry stamped later than the reader's clock sample, treating it as a clock jump. Callers sample time.Now() once and pass it down, so on a daemon starting several tasks at once a peer routinely commits a valid entry dated microseconds after the reader sampled — and the reader, whose own store just succeeded, threw it away and re-ran discovery for nothing. TestOpenclawDiscoveryCacheConcurrentPreparations flakes on CI for exactly this reason; it reproduces locally in a few hundred runs. Tolerate a bounded skew (1s) instead of any future dating at all. That is far above the interleaving window and far below the clock jump the guard exists to catch, and it bounds the cost of being wrong: an entry dated ahead outlives its TTL by at most the tolerance. TestOpenclawDiscoveryCacheFutureDatedEntry pins both edges — a concurrent writer's entry is a hit, a jumped clock is still a miss — so the tolerance cannot be widened into 'never expires' or narrowed back into the flake. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4d5a679b9b |
fix(openclaw): unblock the OpenClaw runtime on slow hosts (MUL-6321) (#7142)
Fixes #7112. Task preparation ran `openclaw config file` and `config get agents.list --json` with a hardcoded 5s deadline; on a 2020 Intel MacBook Pro those take 8.4-10.9s and 4.3s every time, so the OpenClaw runtime was unusable there with no way to raise the limit. - Deadline 5s -> 30s, overridable via MULTICA_OPENCLAW_CLI_TIMEOUT / backends.openclaw.cli_timeout, clamped to [1s, 60s] so the worst case (4 serial calls) stays inside the 5-minute preparation budget. - Config discovery cached per daemon profile (on disk, since preparation runs in a one-shot helper process), invalidated by binary identity, OpenClaw path env, active config stat, and a 60s TTL. Only complete successes are cached; any cache fault degrades to a live CLI run. - The timeout is classified structurally into a new platform-side, non-retryable reason (runtime_cli_timeout) instead of being text-matched into agent_error.provider_network, which showed "check your network" copy and auto-retried a deterministic local failure. Copy added for web/desktop/mobile; a NormalizeDaemonReason shim upgrades reports from older daemons. |
||
|
|
4e10d6805a |
fix(execenv): handle OpenClaw JSON errors on stdout (#7133)
Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
de902a4aab |
MUL-6273: feat(agent): add MiniMax Code ACP runtime (#7035)
* feat(agent): add MiniMax Code ACP runtime * fix(agent): terminate mcode process trees * fix(agent): address mcode review blockers * test(agent): keep mcode model contract API-stable * fix(migrations): resolve mcode prefix collision * fix(agent): launch mcode through runtime command Assisted-by: MiniMax Code * fix(migrations): renumber mcode migration Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
212ecfb8dd |
MUL-6300: fix(daemon): let reply turns own the status arc for their own issue (#7083)
* fix(daemon): let reply turns own the status arc for their own issue (MUL-6300) A comment-triggered run was forbidden from touching issue status: "Do NOT change the issue status unless the comment explicitly asks for it", written in PR #205 (2026-03-31) on the day on_comment became the default trigger. It was aimed at conversation — questions, thanks, agent-to-agent chatter should not churn the board — but it also covered the most common real trigger, a comment asking for rework on a delivered issue. The agent reworked the issue with its status frozen, and a human had to move the card by hand (MUL-5165). Reply mode now owns the same arc Ownership mode does — in_progress when the work starts, the status the work is actually in when the turn completes — but only for turns that carry work on an issue assigned to this agent. The two properties the old prohibition really protected are kept and pinned by tests: - a purely conversational turn never writes status; - a run that is a guest on someone else's issue (@mention pull-in) never writes status, so an agent pulled in to answer a question cannot push another assignee's in-flight issue to in_review. Squad leaders keep routing on the Squad Operating Protocol's "Own the parent issue status" grant, which the server injects only when the issue is assigned to that squad — so the guest-leader path (MUL-3724) stays closed, and the grant no longer has to be phrased as an exception to a prohibition. Also refreshes the two built-in skills that documented the old rule. Supersedes #5781, whose one-line exception was written against brief text that MUL-5377 has since rewritten. Co-authored-by: multica-agent <[email protected]> * fix(daemon): compress reply-mode status rule, cover unassigned issues (MUL-6300) Review round on #7083. The ordinary reply-mode status bullet drops from 103 to 70 words and the squad-leader bullet from 110 to 92, with the conversational-turn sentence now identical in both branches. The no-write invariant broadens from 'assigned to someone else via @mention' to 'not assigned to you': an @mention can also land an agent on an UNASSIGNED issue (triage), which the old blanket prohibition covered but the first draft of the positive rule left undefined. Guard-test pins and the fallback pin in reply_instructions_test.go are synced to the new wording; the arc-trigger substring is unchanged so its two pins survive as-is. SKILL.md carries the same broadened phrasing. Co-authored-by: multica-agent <[email protected]> * docs(daemon): purge stale no-status-unless-asked semantics from ledger surfaces (MUL-6300) Elon's review round on #7083: four surfaces still explained current behavior with the pre-MUL-6300 rule. The squads source map (agent- readable) now says the status-ownership grant, not the system comment's ask, authorizes the leader's wrap-up write; prompt.go's turn-mode comment, squad_briefing.go's grant rationale (which also still pointed at the long-gone writeWorkflowComment), and the child-done stage test comment now describe the conditional arc. Past-tense history quotes (contract-test design note, MUL-6300 replacement note) stay verbatim. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
8b1acfd19e |
fix(execenv): fold redundant metadata-list read into the issue-get step (#7016) (#7042)
Issue-triggered runs paid two reads for the same data: the runtime brief mandated 'issue get' (whose JSON always carries the metadata bag) and then a separate 'issue metadata list'. Fold the read guidance into step 1, renumber shared steps 1-6 to 1-5, and update the brief tests to pin the new wording and ban the retired standalone read step. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
cc2aea3878 |
MUL-6104: fix(daemon): tell the brief's reader not to merge stderr into parsed output (#6870)
Both brief builders now state that `--output json` writes JSON to stdout while confirmations and warnings go to stderr, and that merging them (`2>&1`) into anything that parses the output makes a successful write look failed and invites a duplicate retry. Adds a regression test anchored on the rule's wording — premise, prohibition and consequence — asserted exactly once per rendered brief, so a reversed or duplicated rule fails the test. 234 bytes per rendered brief; a run renders one of the two builders. |
||
|
|
19155e41f9 |
refactor(prompts): compress duplicate --no-start guidance from #6956 (MUL-6168) (#6971)
PR #6956 stated the same rule — use --no-start when the write only records ownership/status of work already underway — five times across the always- injected brief, the sibling-runs block, and the working-on-issues skill. State it once per surface and keep the flag in the command signatures. Prompt-text only; no behavior change. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
b5a48eca13 |
fix(agent tasks): prevent duplicate self-assignment work (MUL-6168) (#6956)
* fix(agent tasks): avoid duplicate self-assignment runs Co-authored-by: multica-agent <[email protected]> * fix(agent tasks): address duplicate-run review Co-authored-by: multica-agent <[email protected]> * fix(agent tasks): resolve sibling-run nits Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
ceb6c6845c |
MUL-6132 fix: stop stranding daemon task markers in user directories (#6917)
* fix(daemon): roll back local_directory sidecars when prepare fails (MUL-6132)
Prepare writes the daemon task marker and the rest of the sidecar tree into
the workdir early, but only persists the manifest that records them at the
very end. Every error return in between left that tree on disk with no record
of what to remove, and since the caller gets no Environment on those paths,
none of the teardown defers that normally undo it were ever registered.
For a local_directory task the workdir is the user's own repository, so the
leftover marker disabled every human-local multica command for that whole
directory tree — silently, and until someone deleted the file by hand. No
crash was needed to reach this state: a failed provider setup was enough, and
writeSidecarManifest failing was only a warning, so a disk or permission
hiccup produced the same permanent residue.
Roll back in place from the in-memory manifest whenever Prepare fails after
writing context files, and treat a manifest write failure as fatal in place so
that rollback runs while the record still exists. Worktree and cloud envs are
untouched: the former discards the whole worktree, the latter is wiped by GC.
Co-authored-by: multica-agent <[email protected]>
* fix(cli): retire stale daemon task markers instead of refusing forever (MUL-6132)
A daemon task marker left behind in a user's own directory made every
human-local command (daemon start/stop/restart/logs, login, logout, setup,
workspace switch, runtime profile set-path/unset-path) fail anywhere in that
directory tree, permanently and with a message that named neither the file nor
a way out. The reported instance sat undetected for five weeks.
Retire the marker automatically, but only on positive evidence that nothing is
running: every profile's daemon must answer and report zero active tasks.
Silence is not absence — an unreachable daemon is what a crashed one looks
like, and a task orphaned by that crash keeps running with this marker as its
only remaining guard, so any unreachable or unparseable profile keeps the
command refused. A minimum marker age closes the window where a daemon that
reports zero is claiming a task and about to write this very marker.
Two things stay out of scope by construction: markers without an agent and
issue id, which is the permanent node-wide guard at the workspaces root rather
than a task leftover, and any invocation carrying MULTICA_* task identity,
where the daemon really did launch this process and no file may override that.
When staleness cannot be proven the command still fails closed, but the error
now names the marker path, matching what newAPIClient has reported since
MUL-3922 and replacing a message that sent the user to the source.
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): arm the in-place rollback before the first sidecar write (MUL-6132)
The rollback was registered after writeContextFiles returned, but that function
writes the daemon task marker as its very first act and can then fail on any
later step — .agent_context, skill files, project resources. Those failures
returned before the defer existed, so the case the fix was written for still
stranded a marker in the user's repository with nothing beside it.
Arm the defer immediately after the manifest is allocated, before any write.
Rolling back an empty manifest is a no-op, which is what makes arming it that
early safe.
Adds a regression test whose induced failure lands inside writeContextFiles
(a plain file where .agent_context must be a directory), one step after the
marker is written. It fails on the previous commit and passes here.
Co-authored-by: multica-agent <[email protected]>
* fix(cli): drop CLI-side marker deletion, keep the actionable message (MUL-6132)
The self-heal could delete the fail-closed guard of a running task. The daemon
increments active_task_count only after a claim returns, so a probe can read
zero for a task that is already claimed and about to write the marker at this
very path; the marker's age says nothing about a file written after the probe.
Serial probing across profiles widened the window further. Separately, the
"every profile answered" proof did not hold: profiles were matched by health
port, and that port is a hash that can collide, so one idle daemon could be
counted as two profiles while a crashed one kept an orphan task running.
Both are properties of doing this from outside the daemon. Removing a guard
needs synchronization with the claim path that only the daemon has, so
retirement moves there and the CLI keeps the half it can do soundly.
What remains is the message, now shared by both refusal paths. Previously only
newAPIClient named the marker file, so whether a user could act on the refusal
depended on which command they happened to run first; requireHumanLocalCommand
sent them to the source instead. Both now resolve the leftover through one
helper and report it identically.
Co-authored-by: multica-agent <[email protected]>
* fix(cli): report leftover markers on every refusal path, never the root marker (MUL-6132)
Two gaps from the previous round's review.
requireTaskLocalConfigRoot is the third path a leftover marker can refuse
through — config show, config set and auth status all call it — and it still
returned a message that named no file. Whether the error was actionable
depended on which command the user happened to run first, which is exactly what
unifying the other two paths was meant to end. It now appends the same suffix
when a marker is the only daemon signal.
The leftover helper also could not tell the two markers apart, because
daemonTaskContextMarkerPath matches on managed_by alone. Running from the
daemon workspaces root therefore described the permanent node-wide guard as a
leftover to delete — advice that closes the hole it exists to cover, aimed at
whoever reads the error, including an agent subprocess that lost its
environment. The helper now requires task identity, defined as agent_id OR
issue_id: a chat task has no issue, so requiring both would misfile its marker
as the root marker, while the root marker has neither field.
chdirWithDaemonTaskMarker was writing the bare managed_by form, which under
this rule models the root marker rather than the workdir marker its tests are
about; it now writes a task-scoped one.
Also corrects two comments the earlier commits left stale: cmd_agent.go still
described an automatic retirement that no longer exists, and
writeSidecarManifest still claimed every caller treats a write failure as
non-fatal, which stopped being true when the in-place path was made fatal.
Co-authored-by: multica-agent <[email protected]>
* docs(cli): correct the refusal-path count in leftover-marker comments (MUL-6132)
Both comments still said "both refusal paths" from when the suffix was shared by
newAPIClient and requireHumanLocalCommand only. requireTaskLocalConfigRoot
joined them in
|
||
|
|
4f10a944b1 |
feat(runtime): add DeepSeek Harness support (#6923)
* feat(runtime): add DeepSeek Harness support * docs(runtime): reflect public DSH release |
||
|
|
85045228a1 |
MUL-6102: fix(daemon): surface which HERMES_HOME a Hermes task actually read (#6892)
* fix(daemon): surface which HERMES_HOME a Hermes task actually read A Hermes task runs against a per-task HERMES_HOME overlay, seeded from a source home the daemon resolves from its own process environment. When that resolution lands on a home holding no config.yaml, the overlay is built anyway from nothing: the derived config keeps only skills.external_dirs, so the user's provider and credentials are gone and Hermes refuses every session/new with "No LLM provider configured". Nothing on that path said so. The overlay was seeded silently, no line logged the resolved source home, and the failure surfaced Hermes' own remedy -- run `hermes model` -- which edits whichever home the user's shell points at, not the one the task read. GH #6872 is what that costs: eight documented workarounds, none of which could have worked, and a root cause filed against an unrelated resume bug. Three additions, no behaviour change: - Warn when the source home is missing, or exists without a config.yaml. Kept a warning rather than a hard failure because config-less is a legitimate setup: an image that ships Hermes and injects OPENAI_API_KEY through the environment resolves a provider fine. - Log the resolved source home once per task, at Info. - Append the overlay and source homes to a "no LLM provider configured" failure, so the task itself names the directory that was read. The annotation runs after every classifier has read the error text. It embeds two user-chosen paths and Classify matches substrings anywhere, so annotating earlier would let a directory name pick the failure_reason -- a home under /srv/token-cache/limit/ satisfies rule 1 (context overflow), which is evaluated before rule 2 (missing config) and feeds retry eligibility and resume blacklisting. The phrase rule 2 already matched is now a shared const so the new predicate and the classifier cannot drift apart. Co-authored-by: multica-agent <[email protected]> * fix(daemon): keep the Hermes hint out of resume-guard input Review caught that the annotation did not stop at the daemon. It travels to the server as the task's error, is persisted in agent_task_queue.error, and is re-scanned there for the life of the row by service.ResumeUnsafeFailure and by the ILIKE/regex guards in GetLastTaskSession / GetLastChatTaskSession. Both interpolated paths are user-controlled -- HERMES_HOME comes from the agent's custom_env, the overlay root from MULTICA_WORKSPACES_ROOT -- so a legal directory name got a vote on session recovery. Measured, with a source home under /srv/400-invalid_request_error/: raw error ResumeUnsafeFailure = false annotated with the paths ResumeUnsafeFailure = true annotated with this hint ResumeUnsafeFailure = false That true is a healthy chat session pointer being cleared on a failure that has nothing to do with it, so "no resume semantics change" was not true as written. The hint is now fixed prose with nothing interpolated, and names the daemon log line that does carry the paths. This removes the vector rather than containing it: no wire or schema change, and it is correct against old servers too, so there is no mixed-version window where an un-upgraded server misreads a new daemon's text. Tests cover the boundary the previous ones missed: classification AND service.ResumeUnsafeFailure agree with the unannotated text across five real failure shapes, plus a check against the SQL guard phrases, which no unit test can execute. Also softens the missing-config warning, which asserted the task runs without credentials when the environment may well supply them -- the config-less-but-working setup this PR deliberately keeps supported. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c9727e6b2b |
feat(daemon): worktree mode for local_directory resources (MUL-5707) (#6759)
* feat(daemon): worktree mode for local_directory resources (MUL-5707) Local directory resources serialise every task on a per-path mutex, so two tasks on one folder queue behind each other. This adds an opt-in worktree mode: each task gets its own git worktree of the user's repo inside the daemon's env root, so tasks run concurrently and deliver their work as a branch instead of editing the user's working copy. Opt in per resource with execution_mode=worktree (default stays in_place, so existing resources are unchanged). Requires a git repo with at least one commit; a non-git directory fails with an actionable message rather than silently falling back to serial execution. Design points that make the mode safe to use: - The agent starts from what the user sees, not HEAD. Tracked edits are captured with `git stash create` (which writes a commit object without touching the index, working tree, or stash list) and replayed into the worktree; untracked non-ignored files are copied, bounded at 2000 files / 200MB with the skipped count logged at warn level. - That replayed state is committed as a baseline, so "did this task change anything?" has an exact answer and `git diff <baseline>..<branch>` is precisely the agent's work. - Nothing is silently discarded: leftovers are committed to the branch before the worktree is removed, including when the task fails partway. - A task that changes nothing leaves no branch behind. - Sidecars stay out of the branch by running the existing CleanupRuntimeConfig + CleanupSidecars pass over the worktree before finalizing. The .git/info/exclude approach repocache uses is not usable here: a linked worktree resolves info/exclude to the user's own common git dir, so it would change what `git status` hides in their checkout. - Worktree-mode env roots do not get the local_directory GC exemption, so they age out normally instead of accumulating one env root per task. - `git worktree prune` on create self-heals registrations orphaned by a crashed daemon. Adds --execution-mode to `project resource add/update`, and updates the docs and the projects-and-resources built-in skill. Co-authored-by: multica-agent <[email protected]> * fix(daemon): close data-loss and isolation gaps in worktree mode (MUL-5707) Review follow-up on #6759. Six issues, all on failure or skew paths. 1. Preserve work when the final commit fails. Finalize logged a warning and then ran `git worktree remove --force`, destroying the agent's changes. commit.gpgSign with no key available to the daemon triggers this, and --no-verify does not disable signing. Finalize now returns an error and deliberately keeps the worktree — it stays registered, so `git worktree list` points at the work — and the daemon fails the task instead of reporting success with a branch that is missing the changes. 2. Fail closed on an unsupported execution_mode. Treating an unknown mode as in_place was wrong: execution_mode is how a user asks for ISOLATION, so the "safe" fallback would edit the working copy they asked us to stay out of. An unrecognised mode now fails the task naming the version skew. 3. Keep Codex conversations alive across turns. The per-issue session store was keyed on LocalWorkDir != "", so worktree tasks — which also never reuse an env root — got a fresh empty CODEX_HOME per turn and silently lost the thread. Covered by a two-turn regression test that fails without the fix. 4. Fail closed on an incomplete snapshot. A failed stash create/apply, a failed untracked copy, or hitting the copy bounds all degraded to "agent sees HEAD" with only a daemon log line. A half-replayed tree looks like a working checkout, so nothing downstream questions it while the agent reads different code than the user has. All of these now abort the prepare and tear the partial worktree back down. 5. Do not snapshot a directory mid-mutation. The same real path can be attached to another project as an in_place resource; snapshotting under a running in_place task captured a half-written tree plus that task's in-flight .agent_context/.multica sidecars, which then reached the branch and could feed the agent another issue's brief. Worktree prepare now takes the per-path mutex for the snapshot alone (released before the run, so tasks still overlap), and the untracked replay skips Multica's own sidecars. 6. Stop the test helper from masking regressions. gitRun skipped on any git failure, including post-finalize assertions, so a lost branch could report as a skip. Git availability is now checked once at repo setup; every later git failure is fatal. Also rebased onto main, keeping both MulticaConfigRoot and LocalWorktree in the Environment initialiser. Co-authored-by: multica-agent <[email protected]> * fix(daemon): close sidecar-commit and wait-visibility gaps in worktree mode; gate worktree saves on daemon version (MUL-5707) - runTask: register the sidecar-cleanup defer immediately after the Finalize defer. Previously two early returns (temp-dir setup, StartTask failure) sat between them, and on those paths Finalize auto-committed the sidecars Prepare had just written — delivering a branch whose only content is Multica's own runtime files (in_place: leaving them in the user's tree). - runTask: give the worktree snapshot wait the same plumbing as the in-place acquire — waiting_local_directory status, resourceWaitTasks accounting, and a cancellation poller. The holder can be an hours-long in_place task; the wait was invisible and a user cancel went unnoticed until the 5-minute prepare timeout. - isMulticaSidecarPath: match sidecar dirs as whole path segments at any depth, not only at the repo root. An in_place resource pointed at a SUBDIRECTORY leaves its sidecars below that subdirectory; replaying them committed another issue's brief into the delivered branch. - handler: refuse saving execution_mode=worktree while the owning daemon's registered cli_version is below MinLocalWorktreeCLIVersion (422, daemon_version_unsupported). An old daemon json-skips the field and runs tasks IN PLACE — the daemon-side unknown-mode refusal cannot protect a daemon that predates the field, so the server has to. Residual (documented): a daemon downgraded after the save needs a claim-time gate. - copyUntrackedFiles: read the -z listing raw instead of TrimSpace-ing it, which corrupted filenames beginning/ending with whitespace into paths that silently vanished from the replay. - docs + skill: describe the save-time version gate; drop the stale claim that old runtimes fail the task themselves. Co-authored-by: multica-agent <[email protected]> * feat(views): UI switch for local_directory execution mode (MUL-5707) Worktree mode shipped CLI/API-only, which for its target user — someone running several tasks at once from Desktop — means it does not exist. This adds the switch to the resources panel. The choice is framed by what the user gets back, not by the mode identifiers: "Edit this folder directly" (one task at a time, changes land in your working copy) vs "Run in parallel, isolated" (tasks run at the same time and each hands back an agent/… branch). `in_place` / `worktree` stay visible as a secondary hint for anyone cross-referencing the CLI or docs. Two entry points, matching where the decision is actually made: - The add-a-folder flow now asks for the mode after the folder is picked and validated, instead of creating the resource and leaving the mode to be discovered later. - Every local_directory row has a mode control. It is deliberately NOT gated on being the owning machine — switching the mode only rewrites a field, so it works from the web app too, unlike rename or the folder picker. Both blocked states are handled where the user chooses, not after they commit: - Non-git folder: the desktop validator now reports whether the path is inside a git working tree (walking up for `.git`, so a subdirectory of a repo counts), and the option is disabled with that reason. Unknown — an older desktop build, or an existing row — stays permissive; the daemon re-checks authoritatively. - Daemon too old: the version floor is mirrored from the server as MIN_LOCAL_WORKTREE_CLI_VERSION and checked against the runtime for the resource's OWN daemon, not the machine the browser happens to be on. A server rejection is rendered inline in the dialog rather than as a toast, so the dialog stays actionable. A "Parallel" badge on the row is always visible (not hover-only): without it there is no way to tell whether tasks on a folder edit it directly or hand back a branch, which is the first thing anyone asks when a task queues. Also adds `execution_mode` to LocalDirectoryResourceRef — the field existed server-side but had no TypeScript counterpart — and brings the zh/ja/ko project-resources docs in line with the English ones, which were the only locale updated when the mode landed. Co-authored-by: multica-agent <[email protected]> * fix(daemon,handler): close worktree isolation and delivery gaps (MUL-5707) Second review round. Five must-fix items, all on paths where the mode's promises could still break. 1. A failed sidecar cleanup no longer ships Multica's own files as the user's branch. The cleanup errors were logged and Finalize then ran `git add -A`, so a permission/disk/lock failure could deliver a branch whose entire content was the runtime brief and issue context. Worktree mode now escalates a cleanup failure to an abort: nothing is committed, the worktree is kept for inspection, and the task fails with the preserved path. In place it stays a warning — no commit, no branch, and the leftovers are visible in the user's own tree. 2. The untracked replay no longer drops files silently. `Lstat` failures and symlinks were `continue`d without counting, so the caller read a partial snapshot as complete. Both now count as skipped, which the existing fail-closed check turns into a refused prepare. Non-content entries (sockets, FIFOs, devices) stay uncounted deliberately. 3. Prepare rolls back a worktree it created. Any later failure — context files, provider home, MCP config — returned without an Environment, so nothing downstream knew a worktree existed, leaving a registration and a branch no task ever ran in. A deferred Discard now runs unless Prepare reached its successful return. 4. The daemon-version gate moved to where it can actually hold. Save-time validation cannot see a machine downgraded afterwards, and an old daemon json-skips execution_mode entirely — it would run the task in place, editing the working copy the user asked to isolate. Claims are now checked against the runtime that is actually claiming, and a blocked task is cancelled with a reason rather than left to be redelivered to the same too-old runtime forever. 5. branch_name is delivered end to end. The daemon sent it, but TaskCompleteRequest had no field so it vanished at decode, and the failure path never sent it at all. It now has a column, travels on both terminal callbacks, and renders as a copyable row in Run details. The failure path matters most: worktree mode commits the agent's leftovers before tearing the worktree down, so a run that died partway still has work worth finding. Docs (all four locales) and the projects-and-resources skill are updated for the claim-time gate and the branch surface. Co-authored-by: multica-agent <[email protected]> * fix(handler,daemon): migration renumber, cancel reasons, branch on every terminal path (MUL-5707) Third review round. 1. Rebased onto main and renumbered the migration to 285 — main had taken 273 and is now at 284. sqlc regenerated on top of the rebased queries; the conflicts were confined to generated files. 2. The claim gate now persists why it cancelled. It called CancelTask, which writes only status='cancelled', and returned the reason as a 4xx that reaches the daemon's log and nothing else — batch claim drops the task from its response entirely. A run refused for a durable reason the user has to act on cannot present as a bare "cancelled", so this adds CancelAgentTaskWithReason and records error + failure_reason on the row. 3. branch_name now survives every terminal path, not just the two direct callbacks. Three were dropping it: - the context-exhausted complete→fail reroute did not copy it; - the daemon's complete→fail fallback (server rejected the complete, agent had succeeded) did not set it; - both cancellation paths discarded the result wholesale, even though the worktree is finalized — and its work committed — before the daemon learns of the cancel. The branch now rides the cancel-ack and is recorded by SetAgentTaskBranchName, which never overwrites a name a terminal callback already wrote. 4. Rollout/rollback compatibility. Isolation is enforced by two server-side gates, so a daemon downgraded at any point after the resource was saved is caught. The gap they cannot close is rolling the SERVER back past this change while sub-floor daemons are still connected: an old server has no gate and an old daemon ignores execution_mode. That constraint is now documented where an operator will meet it (the migration and the docs), and the version floor is pinned across server and frontend by test so the two gates cannot silently drift apart. Co-authored-by: multica-agent <[email protected]> * fix(server,views): route claim-gate cancels through TaskService, make cancel reasons visible, harden cancel-ack branch delivery (MUL-5707) Closes the fourth-round review findings on worktree mode's cancellation chain: - Claim gate: cancel through a new TaskService.CancelTaskWithReason instead of a raw CancelAgentTaskWithReason query, so a gate-cancelled task gets the full cancellation side effects — audit capture, chat settle, agent status reconcile, task:cancelled broadcast, NotifyTaskFinished. Added REAL claim integration tests: singular claim (422 + row cancelled with reason), batch claim (task omitted + row cancelled with reason), and a current-runtime control. - Cancel reasons are now user-visible: execution log rows, agent activity rows, and the transcript dialog label a SERVER-cancelled task (persisted error/failure_reason) like a failed one, with the actionable text on hover and a Reason row in Run details. User-initiated cancels stay a plain "Cancelled". New cancelReasonLabel helper + local_directory_error label; details_reason locale key in en/zh-Hans/ja/ko; widened AgentTask.failure_reason to the open string the wire actually carries. - Cancel-ack is now a terminal delivery: the daemon retries it on transient errors (same schedule as complete/fail), the server fails LOUD (5xx) when persisting the branch/error fails instead of warn-and-200, and re-broadcasts task:cancelled after a successful write so clients that already refetched a branchless row pick up the delivery. Replays are idempotent (never overwrite). - Cancel + Finalize-abort no longer swallows the preserved-worktree pointer: the finalize error is wrapped in worktreePreservedError (joined onto an existing run error, never displacing it), extracted on the cancel path, and delivered via the ack into agent_task_queue.error through the new SetAgentTaskErrorIfEmpty query — never overwriting a reason recorded by a fail callback or the claim gate. Other run errors on a cancelled run stay discarded as before: they are expected noise. - Docs: the self-hosted rollback constraint callout now exists in zh/ja/ko, not just English. Co-authored-by: multica-agent <[email protected]> * fix(handler): guard cancel-ack writes to cancelled rows; requeue when the claim gate's cancel fails (MUL-5707) Fifth-round review fixes: - SetAgentTaskBranchName / SetAgentTaskErrorIfEmpty now carry a status='cancelled' CAS. The daemon acks on EVERY terminal status it observes, so a late or replayed ack from a stale run could previously backfill its branch or preserved-path error into a completed/failed row whose own callback — the authoritative channel — recorded nothing, without any event invalidating the change. A CAS-refused write is a deliberate no-op (still 200: nothing to retry), and the rebroadcast stays guarded by the same status check. Regression tests: a late ack against a completed row with empty fields and against a failed row leaves both untouched. - When the worktree claim gate's CancelTaskWithReason fails, the claimed row no longer strands in dispatched (daemon refused, nothing to release it but stale reclaim). The gate requeues via RequeueTaskAfterClaimFailure — the next claim re-runs the gate and retries the cancel — and reports 5xx so the singular daemon reads a transient server problem, not a claim refusal; the batch path omits the task as before, now with the row requeued. Fault injection tests (row-scoped trigger raising on the cancel UPDATE while letting the requeue through) cover singular and batch. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
28a7176328 |
MUL-6053: fix(daemon): give Hermes conversations a session that survives the task (#6845)
* fix(daemon): give Hermes conversations a session that survives the task Hermes keeps every ACP session in `<HERMES_HOME>/state.db`. Because the daemon takes over HERMES_HOME to inject bound skills, that database lived inside the per-task overlay, so a conversation lasted exactly one task. The failure was silent, which is what made it expensive. `session/resume` against a database that does not hold the session does not fail: Hermes creates a fresh session and answers with an ordinary success frame, and ACP's ResumeSessionResponse has no `sessionId` field to contradict it. The daemon therefore believed it had resumed, stored the same dead id again, and every turn restarted from zero with nothing in the logs (GH #6806). Three changes, in the order they matter: - The backend now reads `_meta.hermes.sessionProvenance.acpSessionId`, the only place Hermes reports the session it actually served. A turn that landed on a different session continues under the new id and carries the existing ResumeContinuityNotice, so the agent can read the transcript back instead of pretending it remembers. - `state.db` is linked to a per-conversation store at `<profile dir>/hermes-sessions/<agent>/<hermes profile>/<conversation>/`. Sharding per conversation is what makes this safe where #6693 left it out of scope: tasks of one conversation are serial, so a shard has a single writer, while two issues never share a database. SQLite resolves the link when deriving the WAL paths, so the journal sidecars live in the store and a task teardown cannot take the write-ahead log with it. Windows without symlink privileges keeps the database task-local rather than copying it. - `gateResumeToReusedWorkdir` no longer treats a matching workdir as proof the session is reachable. The two keys came apart in the local_directory flow, where workdir reuse is disabled but envWorkDir still equals PriorWorkDir, so the gate forwarded an id that could never resolve. Stores are GC'd on their own retention (`MULTICA_GC_HERMES_SESSION_TTL`, default 14d, matching the Codex session store) and held against the GC for the duration of a task. Co-authored-by: multica-agent <[email protected]> * fix(daemon): never destroy a Hermes session database on a failed mount Review of #6845 found three ways the new session store could lose or misreport a conversation. All three are on failure and reclaim paths that the original tests did not reach. - **The degradation path was destructive.** The mount migrated the task-local state.db into the store and deleted it, and only then tried to create the symlink. On a host that cannot symlink, that turned a conversation which previously survived via workdir reuse into an empty one — a regression on exactly the hosts the fallback exists to protect. The link is now created under a staging name first, and nothing destructive runs until it exists; a host that cannot link is left byte for byte as it was found. - **The migration was not atomic.** It copied state.db and its journal sidecars straight into the store, so a failure after the main database landed left a store that looked migrated. The next attempt skipped the migration, deleted the source, and took the un-checkpointed tail of the conversation with it. It now stages the whole family and publishes with one rename, reusing the memory store's staging helpers (renamed to drop their `memory` prefix now that both stores share them). - **"Mounted" was read as "resumable".** A store the GC reclaimed between turns, a switched Hermes profile, an operator's `rm`, or a dangling link from an older overlay all mount cleanly onto nothing, and the resume gate forwarded a dead session id into them — the same silent restart this change set out to fix. The mount now reports two separate facts, and the gate reads whether the store actually holds a transcript, not whether the plumbing succeeded. A zero-length database counts as no history. Tests cover each path with injected failures rather than host-dependent skips: a link failure preserves the database and its WAL and still mounts once linking works again, a migration that dies between the database and its WAL publishes nothing and loses nothing on retry, and a mounted-but- empty store (fresh, reclaimed, dangling, zero-length) reports no history. Verified non-vacuous — reverting each fix reproduces the reviewed symptom. Co-authored-by: multica-agent <[email protected]> * fix(daemon): make the Hermes session store agree with itself on "empty" Review of the previous commit found a certain data-loss branch left between the migration and its publish step. The two used different definitions of an occupied store: - the migration asks hermesStoreHasSessionDB — a zero-length state.db, or journal sidecars with no database, carry no transcript, so it proceeds; - the shared promote asked hermesStorePopulated — any directory entry at all — so the same remnants read as "a competitor published first". With a non-empty source and a zero-length state.db in the store, the staging copy completed, os.Remove(storeDir) failed on the non-empty directory, promote reported a lost race, the migration returned success, and the caller deleted the source database and its un-checkpointed WAL. The link then pointed at a database with nothing in it. Occupancy is now the caller's predicate: the memory store keeps "any entry", the session store passes its own "holds a real database". The session publish clears the remnants it has already classified as nothing before the atomic directory rename, and a store it cannot clear fails closed — the migration returns the error and the source stays put — rather than passing for a lost race. The regression test drives the whole matrix through prepareHermesHome: a zero-length database, orphan sidecars, a real database that must never be overwritten, and an unclearable store that must fail closed with the source intact. It fails on the previous commit with the reviewed symptom (HistoryPresent=false after the source was deleted). Co-authored-by: multica-agent <[email protected]> * fix(daemon): never delete a competitor's freshly published Hermes transcript publishHermesSessionStaging decided the store held no history, then deleted the state.db family by path name. Between those two steps a competing task could publish its real transcript, and the stale decision would remove the winner's database — after the winner had already reported success and discarded its source. The check, the remnant cleanup and the promote now hold one process-wide mutex. In-process exclusion is the real thing here, not an approximation: the daemon is the store tree's only writer (even the GC guard is in-memory refcounts on the Daemon), so two publishers can only ever be two goroutines of this process. A deterministic barrier test parks a publisher inside the critical section and proves a competitor cannot enter the window. The family cleanup also stops using RemoveAll: regular files and symlinks are unlinked (never followed), while a directory or any other irregular type under a state.db name is content this code cannot identify, so it fails closed and the caller keeps its source. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> Co-authored-by: Steve Jobs (Multica Agent) <[email protected]> |
||
|
|
8d292ef7ce |
MUL-5963: feat(server): let web chat read back history via multica chat history (#6680)
* feat(server): let web chat read back history via `multica chat history` When an agent's provider session resume fails on a web chat / Feishu task, the daemon injected SessionContinuityNoticeUnrecoverable, making the agent reply "this is a new session, no memory". But the conversation was never lost — it is persisted in chat_message, and the agent just had no command to read it back (only Slack had a history reader). Make `multica chat history` serve the stored chat_message transcript for any non-Slack session (web chat, Feishu), reusing the existing task-token authorization and the shared response shape. Route web chat / Feishu continuity notices to a new variant that tells the agent to read the transcript back instead of scripting a "new session" disclosure, and teach the Feishu channel prompt the read-back command. - handler/chat_history.go: GetChatChannelHistory falls back to chatMessageHistory (ListChatMessages, oldest-first, default recent 30) on ErrNoSlackSession. - execenv: add SessionContinuityNoticeChatTranscript; keep Unrecoverable for channels Multica stores no transcript for (e.g. WeCom). - prompt.go: route web chat / Feishu to the transcript notice; Feishu channel awareness teaches `multica chat history`. - tests: update surface/notice matrix, Feishu prompt policy, add handler transcript fallback tests. Co-Authored-By: Claude Fable 5 <[email protected]> * test(server): fix transcript read-back test to insert a real assistant row The insertChatVisibilityMessage helper hardcodes role='user', so the "assistant reply" row in TestGetChatHistory_NoSlackBindingReadsStoredTranscript was actually stored as a user message and the handler (correctly) returned role "user". Add an insertChatMessageRole helper and use it for the assistant row so the role-mapping assertion is meaningful. Co-Authored-By: Claude Fable 5 <[email protected]> * refactor(server): treat WeCom as a readable transcript surface for session continuity WeCom is fully wired on main: it persists the conversation to chat_message and stamps ChatChannelType="wecom", so `multica chat history` reads it back just like Feishu's. Stop routing it to the Unrecoverable fallback; give it the transcript notice and the `multica chat history` history command, symmetric with Feishu (no thread reader, no upload). Co-Authored-By: Claude Fable 5 <[email protected]> * fix(server): make transcript read-back reachable and honor the paging contract Addresses review on the web-chat history read-back feature. - GetChatChannelHistory no longer dead-ends on "no channel integration" when SlackHistory is nil: a no-Slack deployment (the exact target of this feature) now serves the stored chat_message transcript directly, and Slack-backed sessions still go through the Slack reader. - chatMessageHistory swaps ListChatMessages for ListChatMessagesPage, honoring the ?before cursor (encoded as created_at|id) and clamping ?limit (default 30, max 50), and advertises NextCursor on a full page so an agent can page back without re-reading the recent window. Author now uses the channel vocabulary (Bot / User) instead of the raw role string. - Adds a single SurfacePersistsTranscript predicate (web chat, Feishu, WeCom, DingTalk) as the one source of truth for "which surfaces are readable", shared by the continuity-notice router, the chat-prompt history copy, and the surface list — closing the DingTalk gap where the notice said "read it back" but the prompt said "no history reader for dingtalk". Tests: nil-reader-with-rows regression, transcript paging without duplicates or gaps, Author/TS pins, and DingTalk cases in the surface/notice and prompt tables. Co-Authored-By: Claude Fable 5 <[email protected]> * test(server): fix transcript paging test assertion The paging test wrongly asserted messages are strictly oldest-first ACROSS pages, but paging to older messages means each later page is older than the previous one — the sequence is not monotonic. The handler was correct; the assertion was wrong. Now it checks the real invariants: each page is oldest-first, and the union of all pages is exactly the transcript with no duplicates or gaps. Co-Authored-By: Claude Fable 5 <[email protected]> * fix(server): stop telling transcript surfaces their history is not in Multica The channel-awareness block opened with "This conversation and its history live in <platform>, NOT in Multica" for every channel type, and the very next line told a Feishu/WeCom/DingTalk agent that Multica stores the conversation and hands it `multica chat history` to read it back. An agent that believes the first sentence has no reason to run the command in the second. Only the useful half — do not go hunting through issues and comments — is unconditional now. Where the conversation actually lives moved into each branch: Slack keeps the original wording (true there), transcript surfaces say the conversation happens on the platform and Multica stores a transcript of it, and the defensive fallback keeps both claims for a surface that really has no reader. Co-authored-by: multica-agent <[email protected]> * fix(server): name the channel a served transcript came from HistoryPage.ChannelType is documented as empty ONLY for a session bound to no channel, but the transcript branch never set it. A WeCom/Feishu/DingTalk session that HAD messages therefore reported channel_type "" — telling the agent it was in a web-only chat — while the same session's empty read already named the platform. One endpoint, two answers, decided by whether the session happened to have messages. Reuses the existing sessionChannelType helper, so the transcript path inherits its refusal to guess: a binding read that fails for any reason other than "no such row" is reported rather than silently answered as web-only. That failure now returns the same status as the empty-read path does for the same cause, instead of the 502 reserved for a failed channel read. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: qiushihao279-cloud <[email protected]> Co-authored-by: Claude Fable 5 <[email protected]> Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
84dc02cd65 |
MUL-6034: fix(cursor): normalize MCP approval server shapes (#6776)
Cursor normalizes an MCP server config into a fixed field set and order before
hashing it into an approval key, so equivalent configs written in a different
field order produced keys Multica never matched, leaving the server unapproved.
Serialize stdio servers as {type?, command, args?, env?, cwd?} and remote
servers as {type?, url, headers?}, dropping unknown fields, and marshal without
Go's HTML escaping so the bytes match Cursor's JSON.stringify output.
Approval keys verified against cursor-agent 2026.08.04-aaa8809 and
2026.07.20-8cc9c0b via `mcp enable`; expected hashes in the tests are captured
from a real Cursor install.
Reported and fixed by @wuchangming.
|
||
|
|
b904e6b71c |
MUL-6029: docs(daemon): correct two comments #6604 made false, and enforce the Slack/Lark brief policy (#6771)
Three follow-ups to #6604: two comments the merged diff made false, and one policy the tests stated but did not enforce. - `handler.go` said the brief turns `chat_channel_delivers_files` into upload-or-describe copy. After #6604 the per-turn prompt does, and the brief states no verdict at all — it is the prompt cache prefix, and a per-turn verdict there renders two briefs (MUL-5377). - `outbound_media.go` cited `TestDeliverAttachments_AdmissionBoundsTheLookupStage` for a sentence about turns carrying no file; that test's turns all carry one. The paragraph now states both ways admission is held without a pending slot and cites the one the test drives. - `TestBriefSurfaceDeliveryPolicy`'s `chat_slack` and `chat_feishu` rows denied two phrases where the WeCom rows deny four, so a denial worded around "does NOT apply" passed. Both rows now carry the same four. Review follow-up (tests only): `TestBriefChannelDeliveryCopyIsPlatformNeutral` holds the same policy structurally — with the display name substituted out, every channel-backed brief must be byte-identical, so a per-platform position fails whatever wording it uses. Slack/Lark `mustHave` reaches parity with WeCom, `mustNot` failures quote the real match instead of a section it may not be in, and the admission test asserts `pending == 0` rather than implying it. No behaviour change: two comments and test coverage. |
||
|
|
90eadd940a |
MUL-5905: feat(wecom): send the files an agent produced into the chat (#6604)
* feat(wecom): send the files an agent produced into the chat An agent that produced a file and answered in WeCom delivered only the sentence. `multica attachment upload` had already run, the object was in storage, and CompleteTask had bound the row to the assistant message — but nothing downstream of that bind knew how to reach a WeCom conversation, so the file stopped there. The user read "here is the forecast" and got no forecast, with no error anywhere to explain it. Worse when the agent said nothing at all: an empty completion with a file bound to it returned early, so the artifact was thrown away silently. This adds the last hop. media_upload.go carries a file up the long connection — WeCom has no REST endpoint for this, so it goes as aibot_upload_media_init / _chunk / _finish over the same socket, and all three read their verdict through the request() layer #6590 generalized. outbound_media.go is the caller: after the answer is out, it reads the bound attachment out of object storage, decides what to call it, and pushes it in as its own message. Three decisions worth naming. The answer goes first and the file never blocks it. An upload is megabytes and round trips and it can fail; delivery runs after the words are out, on its own goroutine and a budget of its own, and its worst outcome is one extra line saying a file did not make it. A lost ack and a refusal are opposite failures. A chunk whose verdict never came is offered once more — the protocol makes re-sending idempotent, and losing one slice of a hundred must not cost the file. A chunk the server refused is not, because the refusal is its answer and will be its answer again. The push of the finished media follows the same rule in reverse: a timeout is reported as-is rather than retried, because the same media_id sent twice puts the file on the user's screen twice with nothing to undo. WeCom validates bytes against the msgtype, so what to call a file is a decision. A kind past its ceiling is demoted to a plain file rather than sent and refused — a card the user can open beats a photo the server rejected. WithAttachments is what turns this on, and router.go passes it only when the deployment configured object storage. Without it the adapter behaves exactly as it did before. * fix(daemon): stop telling a WeCom agent its conversation is text-only The per-turn chat prompt and the runtime brief both decided file delivery by asking "is there a channel at all". Any non-empty ChatChannelType got the text-only paragraph: `attachment upload` does not apply here, describe the file in words, never write its local path. That was true of every IM channel when it was written, and it stopped being true for WeCom, whose adapter now reads the bound attachment out of object storage and sends it into the room. So an agent on WeCom was instructed not to use the one command that would have delivered its file. The capability shipped and the agent was told it did not exist. The delivery question is per-adapter, not "is there a channel", and ChannelCarriesFiles is where that answer now lives. Both call sites branch three ways instead of two. The new copy is deliberately built from main's existing web-chat branch rather than written fresh. Both open on the same clause — "To include a file or image you produced in your reply, run `multica attachment upload <local-path>`" — and diverge exactly where the platforms diverge: web chat says the file "appears as an attachment card below it" and offers a markdown snippet to place it inline; WeCom says Multica "sends it into the WeCom conversation as a separate message right after your text" and that "there is no way to place it inline". That last clause is the one worth having. An agent told only that files work here writes "see the chart below" and then nothing appears below, because the long connection has no msg_item and a picture cannot be embedded in the reply. Slack and Lark keep their existing text-only wording untouched. Two comments are rewritten rather than carried over, because they asserted the binary the same commit falsifies — "keys off 'is there a channel at all'" in prompt.go, and "any non-empty channel type means the reply leaves Multica for an external IM platform, where `attachment upload` has nothing to bind to" in the brief. A comment that contradicts the code beneath it is worse than none. ChannelCarriesFiles names every channel type this package knows rather than leaving Slack and Lark to the default, so a fourth adapter surfaces here as a decision to make. An unrecognized type answers false: the agent is told to describe its file in words, and the worst case is a deliverable that could have been sent was not. False the other way would have it promise a delivery nothing performs. * fix(wecom): stop claiming more certainty than the system has Three review blockers, one shape: a promise made in one place that another place cannot keep. Capability came from the channel type. The daemon answered "this conversation takes files" from `chat_channel_type == "wecom"`, but the last hop only exists where the server wired object storage — so a WeCom deployment without storage had its agents told to send files nothing would carry, and a new daemon on an older server inferred a capability that build has no code for. The server now answers it per turn on the claim (`chat_channel_delivers_files`), declared by the same `if store != nil` branch that gives the adapter the storage, and absent/false everywhere else. Upload limits came from the framing. 50 MB is what 100 chunks of 512 KB can describe; WeCom's own OpenClaw plugin puts the hard file ceiling at 20 MiB, so that is now the cap, enforced against the attachment's recorded size before any byte is read. Concurrency was a fixed 4 regardless of size; WeCom's own SDK steps it down — all at once up to 4 chunks, 3 up to 10, 2 past that, because their backend answers a burst of chunks on a large upload with a system error. Failure handling had two words for three outcomes. A send whose ack never came was reported as a definite failure although the file may well be in the chat, and the local failures — a full pending queue, a slot wait that expired, a lookup that errored — were reported to nobody at all. There are now three states: delivered says nothing, definitely_failed keeps the definite wording, and unknown gets wording that holds either way and is never resent, since a duplicate file cannot be taken back. The attachment lookup moved ahead of the rationing so a shed delivery knows there was a file to apologise for, and a turn with nothing attached still says nothing. * fix(wecom): bound the attachment lookup, and stop calling an attempted write a failure Two gates, not one better place for one gate. The pending counter is claimed after the lookup so a shed can name a real file, which is also why it cannot bound the lookup: at the point it would have to be claimed, whether the turn carries a file is exactly what is not yet known. admitAttachmentDelivery is claimed before the spawn and bounds the goroutines and the queries they run. Its cap is twice the pending cap, so a backlog meets the honest post-lookup shed first and only reaches the silent one when the system is already well past it. A refusal there is logged and not spoken — telling a user their file was dropped on a turn that may have carried none is the false alarm the post-lookup gate exists to prevent. Separately, a WebSocket write error stopped being read as proof of non-delivery. wsSender.write already distinguished the stages for tracing; it now carries that out in the error. Past WriteMessage the frame may be at the peer — a half-closed connection reports "broken pipe" to the writer for bytes the reader has — so those classify as unknown and are never resent. What fails before the write is provably local and stays definite. Tests drive the real path rather than the counters: one blocks the lookup, submits past the cap, and counts the goroutines and queries that actually happen; one hands the frame to a connection that records it and then fails. Both fail without the change. fakeObjectStore takes a lock, because it is no longer only ever driven inline. * docs(wecom): cite the size limits to WeCom's docs, not a third-party constant The upload ceilings were sourced to Tencent's OpenClaw plugin. That file carries the right numbers but cites nothing itself, so it corroborates rather than establishes them. WeCom documents all four on aibot_upload_media_init's body.total_size row, and repeats them on the classic media upload API. Point at those. * fix(wecom): drop the two media helpers #6605 brought in ahead of this branch This branch carried its own cleanShort filename and extension helpers because nothing else in the package had them when it was written. #6605 has since merged, and it brings both — so the package now declares each twice and does not compile against current main. GitHub still reported the branch mergeable and CI was green, because both were measured against a main that predated that merge by six hours. Upstream's copies are the ones to keep: its cleanMediaFilename also strips control characters out of a name, and its mediaExtension drops a charset parameter before matching. Neither of those was in the copies here. * fix(daemon): keep the file-delivery verdict out of the cached brief chat_channel_delivers_files arrives on every claim and answers a deployment question: does this server carry a file the last hop into the room. Upgrading a server so it starts sending the field, or turning object storage on or off, flips it under a chat session that is already running — and the brief branched on it, so one resumed session rendered two different briefs. The brief is the prompt-cache prefix (MUL-5377), which makes that a miss on every turn after the flip. The two rows in the delivery test asserting different briefs for the two verdicts were the exposure written down: they are what one session sees in sequence. The brief's channel-backed chat copy now names the platform and says the answer is stated per turn. It holds on either verdict and contradicts neither. The verdict has a home already — the per-turn chat prompt, whose branch was correct and is untouched — and that message is emitted on every turn, so nothing is lost by the brief declining to answer. Slack and Lark take the same copy. Scoping it to WeCom would put a channel-type decision back into the brief, which is what ChannelCarriesFiles exists to prevent, and would hardcode a denial a server-side change can falsify. Coverage moves rather than goes. TestBriefByteIdenticalAcrossRunsForEveryKind gains a WeCom kind and a variant that flips the field; both fail without this change, and the existing Slack kind fails on the variant too. The delivery test's two WeCom rows assert the inverse property now — one brief for both verdicts — and the upload/describe split they used to hold is pinned by TestBuildChatPromptTwoLayerChannelPolicy on the per-turn prompt, which has covered both branches all along. * docs(wecom): correct what the admitted cap implies, and note that delivery cannot answer back The comment on the two counters said reaching the admitted cap means the pending cap is full and as many turns again are still in their lookup. It does not. Admission is held for a goroutine's whole life, including turns whose lookup finds no file and which therefore never claim a pending slot — TestDeliverAttachments_AdmissionBoundsTheLookupStage fills admission with pending at zero. The two-counter split and the ordering it buys are unchanged: a backlog of file-carrying turns still meets the honest post-lookup shed first. The package doc gains the fact that an agent cannot learn its delivery was refused. `multica attachment upload` returns once the object is in storage and bound to the reply, while the send into the room runs on EventChatDone, after the run has ended, so everything the adapter knows about a failed delivery goes to the person in the chat and to nobody else. |
||
|
|
e0d0b38153 |
fix(daemon): keep the runtime owner's Reasonix deny rules in a task (MUL-6021) (#6764)
* fix(daemon): keep the runtime owner's Reasonix deny rules in a task The per-task reasonix.toml denies the `ask` tool, but Reasonix resolves the project file over the user config, so `deny = ["ask"]` replaced the runtime owner's own deny list instead of extending it: an owner who had denied `bash` globally got it back inside every Multica task. The task config is now rendered from the owner's own [permissions] table with `ask` merged into deny. The whole table is restated, not just deny, so the policy survives whether Reasonix layers these files per key or per table, and the table is read from the same REASONIX_HOME the child gets (the agent's custom_env override, else the daemon's env, else the default). When that config cannot be read or restated faithfully — unreadable, malformed, or a deny list that is not an array of strings — the daemon writes no project config at all rather than one that would drop the owner's rules. The task then runs with `ask` available, exactly as it already does when a repository ships its own reasonix.toml. Co-authored-by: multica-agent <[email protected]> * fix(daemon): resolve the Reasonix user config the way Reasonix does Restating the owner's permissions only helps if the daemon reads the file the Reasonix child actually loads. The first cut assumed $REASONIX_HOME/config.toml with a ~/.reasonix default, which misses: - Windows, where the home is %AppData%\reasonix (falling back to %USERPROFILE%\AppData\Roaming\reasonix), not %USERPROFILE%\.reasonix; - the legacy OS-support and XDG locations Reasonix reads when the primary config does not exist and REASONIX_HOME is unset; - an agent that sets REASONIX_HOME to empty, which sends the child to the platform default while the daemon kept reading its own. Each miss reads "no owner permissions" and writes a task config holding only deny = ["ask"] — the exact widening this change set exists to stop. reasonix_user_config.go now ports Reasonix's own resolution (upstream internal/config/paths.go): REASONIX_HOME with ${VAR} / ${VAR:-default} expansion, ~ handling and absolute normalization, the per-platform home defaults, and userConfigLoadPath's legacy fallbacks with the isolation rule that switches them off once REASONIX_HOME is set. The lookup runs against the sanitized custom_env layered over the daemon's environment — the same env the child is launched with — so presence, not just value, decides where to read. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
8e96329196 |
refactor(daemon): drop the MULTICA_HERMES_TASK_MEMORY rollback switch (#6760)
The switch let an operator restore the pre-MUL-5932 behaviour of a fresh task-local memories/ per task. Hermes agent memory migrated correctly, so it no longer earns its keep — and it never covered the failure it looked like it covered: the overlay already depends on directory links to mirror the user's ~/.hermes, so a host that cannot create them fails at the mirror step regardless of this env. Flipping it was also lossy in both directions: with the switch on the daemon stopped marking the store in-use and refreshing its mtime, so the GC could reclaim the agent's memory after GCHermesMemoryTTL; flipping it back discarded whatever the task-local dir had accumulated, because a populated store is never migrated into. The task-local path itself stays — it is still what a task with no agent to key on, or an unresolvable Multica profile dir, gets. Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
807b8706bb |
MUL-5992 fix(daemon): deny the Reasonix ask tool per task (supersedes #6745) (#6746)
* Revert "fix(agent): keep Reasonix off the ask tool in unattended tasks (#6745)"
This reverts commit
|
||
|
|
a1da8e536f |
MUL-6000 fix(daemon): link user Codex skills into codex-home instead of copying them (#6742)
* fix(daemon): link user Codex skills into codex-home instead of copying them Every Codex task got a full recursive copy of ~/.codex/skills/ inside its per-task CODEX_HOME. For a user with npm-backed skills that is ~100 MB per task directory, re-copied on every task start — hydrateCodexSkills wipes and re-seeds on both the Prepare and the Reuse path — and never reclaimed: the GC artifact sweep only matches node_modules / .next / .turbo, and whole-directory reclaim needs the issue to reach a terminal status first. Total cost scales with the number of live task directories. Link instead. The same per-task home already links its other two shared resources, sessions and the plugin cache, and a skill directory is read-only input to the CLI, so the write isolation a copy incidentally bought has no requester. EvalSymlinks still runs first, so an installer-shipped symlink (lark-cli into ~/.agents/skills/) resolves to the real directory rather than producing a link to a link. The three paths that could reach through the new link are unchanged in behavior: os.RemoveAll deletes a link and not its target, writeSkillFiles probes with os.Lstat so a workspace skill colliding with a linked user skill lands in a `-multica` sibling, and the GC refuses to walk into links. Tests pin all three, plus a windows-tagged test for the junction form of the link. Co-authored-by: multica-agent <[email protected]> * fix(daemon): stop GC walks from descending into Windows directory junctions The GC's artifact sweep and its size accounting both refuse to follow links out of a task directory, but they tested for os.ModeSymlink alone. That misses the link shape Windows actually produces: createDirLink falls back to a directory junction (mklink /J) whenever os.Symlink is denied, which is the default without Developer Mode, and since Go 1.23 os.Lstat reports a junction as ModeDir|ModeIrregular with no ModeSymlink bit while its DirEntry still answers IsDir() == true. filepath.WalkDir therefore descends straight into the link target, and the sweep would delete a node_modules / .next / .turbo directory that lives inside the user's real home. Match both mode bits in all three walks. The task directory links user content in three places today — the Codex session store, the plugin cache, and now the user's skills — so this is reachable on any Windows host running Codex tasks. Windows-tagged tests create the junction with mklink /J directly, so the junction shape is covered even on a runner where symlinks are permitted, and they run in the existing windows-execenv CI job. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
673104d26e |
docs(daemon): correct the Hermes overlay's skill-less claim (#6703)
The Hermes memory docs said an agent with no skills bound gets no overlay and keeps using the host's ~/.hermes. The daemon does gate the overlay on the task carrying skills, but the server appends the platform's built-in skills to every agent (LoadAgentSkillBundles), so a claimed task's skill set is never empty and that branch is unreachable in production. Two consequences the old wording got wrong: every Hermes agent gets the overlay, and therefore the persistent memory store from #6693, not just ones with user-bound skills; and clearing an agent's skill list is not a way to keep using the host's memories. Reported on #6638, where the same sentence was read as a contradiction. Docs only, plus a note at the gate so the next reader does not take the skill-less branch for observable behaviour. |
||
|
|
f26b97e395 |
MUL-5932: fix(daemon): give Hermes agents memory that survives the task (#6693)
Hermes discovers skills only from its own home, so Multica takes over HERMES_HOME to inject an agent's bound skills. The overlay inherited the decision of where memory lives and made it task-local, so a skill-bound agent forgot everything between tasks while a skill-less one shared the host's memories with every other agent on the runtime (#6638). Link the overlay's memories/ to a store Multica owns, keyed on (hermes profile, agent), mounted by directory link with its own GC TTL and the shared active-store guard. Memory now survives across tasks and issues for one agent, never touches the user's home, and is invisible to other agents. Session history (state.db) stays task-local, and skill-less agents are unchanged. MULTICA_HERMES_TASK_MEMORY=1 reverts; MULTICA_GC_HERMES_MEMORY_TTL bounds retention (90d default). Migration of a pre-store memories dir copies into a staging sibling and publishes with a single atomic rename, fails closed on any I/O error or entry it will not carry, and never infers a lost race from a failure — the source directory is only removed once its contents are confirmed in the store. |
||
|
|
3dd128532c |
feat(agent): add oh-my-pi (omp) as a supported agent runtime (#6514)
* feat(agent): add oh-my-pi (omp) as a supported agent runtime oh-my-pi (omp, https://omp.sh) is a separate CLI that is a drop-in fork of pi and speaks the same JSON event protocol. It is registered as an independent provider so a host with both pi and omp installed gets two runtimes instead of one. omp is a runtime identity on the pi protocol, NOT a new protocol family. It is declared in a single BuiltinRuntimes descriptor (server/pkg/agent/builtin_runtimes.go) that carries id, protocol_family, default_command, env_prefix, display_name, skills_dir, user_skills_dir, launch_header, backend overrides, and model_discovery strategy. Every consumer (agents_probe.go, config.go, daemon.go, execenv/context.go, local_skills.go, agent.go New/LaunchHeader, models.go ListModels) derives from this descriptor — no per-file hardcoded omp special cases. NewRuntime() is the typed entry point for runtime identities: it resolves id → descriptor → protocol family backend → applies overrides through a backendOverrideApplicator interface (piBackend implements it). New() delegates to NewRuntime() for built-in runtime identities, keeping New() meaning exactly one thing: the protocol-family factory. Model discovery: parseOmpModels reads the real {"models":[...]} wrapper with separate provider/id/selector/name fields. Model.ID is the selector (provider/id), matching parsePiModels' convention so buildPiArgs emits both --provider and --model — not just --model. A regression test (TestOmpSelectorSurvivesToBuildPiArgs) pins this end-to-end. Frontend: provider-logo.tsx maps omp to PiLogo. The transcript dialog uses the shared providerDisplayName helper. Landing copy updated to 21 tools in all 4 locales. Tests (omp_test.go): descriptor dispatch, NewRuntime entry point, binary resolution, omp-labeled errors, event-stream completion, pi+omp side-by-side registration, selector→buildPiArgs regression, model parser coverage (real JSON/empty wrapper/invalid stderr/duplicate), and a guard that every descriptor field is non-empty and consumed. Closes #3989 * fix(views): keep Claude Code label in the transcript run details Switching the provider row to the shared runtime formatter renamed every Claude run: the daemon has no display-name override for claude, so the shared formatter answers "Claude", and the legacy claude-code value title-cased into "Claude-code". Both read as "Claude Code" before. Keep those two aliases in a table local to this view and defer everything else to the shared formatter, so the row stays in lockstep with the runtime list (#5260) without renaming Claude runs. * test(daemon): assert pi and omp both reach the register payload The existing omp test stopped at probeAgentCLIs, so nothing covered the part a user actually sees: whether both runtimes are registered with the server, each under its own display name. Discovery finding two entries and New() building two backends both stop short of version detection and the registration payload. Add a daemon-level test that runs real discovery off a fake PATH, drives syncWorkspacesFromAPI, and asserts the payload carries type=pi and type=omp with names "Pi" and "Oh-My-Pi". Rename the discovery test to say what it covers, and record the registered display name in the batch fixture so the name can be asserted. * docs(agent): correct descriptor comments that describe the old behaviour Three doc comments still described behaviour the fail-closed rework replaced: - ModelDiscovery claimed a nil strategy falls back to the family's discovery; ListModels deliberately returns an empty catalog instead, because omp exits non-zero on pi's --list-models. - backendOverrideApplicator claimed backends without it are returned unchanged; NewRuntime returns an error rather than dropping the descriptor's executable and label. - ProtocolFamily credited New() with applying the ID-specific defaults; that moved to NewRuntime. Also repair the truncated first sentence of the NewRuntime doc, and fix discoverOmpModels still calling the output a JSON array when parseOmpModels right below it documents the {"models":[...]} wrapper. * style(server): gofmt the files this branch touched Four files were left unformatted: the descriptor literal and the omp test struct lost their key alignment, execenv/context.go had its new import out of order, and local_skills.go kept the old indentation after its switch moved inside an if/else. Whitespace only — no behaviour change. --------- Co-authored-by: Bohan-J <[email protected]> |
||
|
|
586a624aa5 | fix(cli): isolate task profile state (#6407) | ||
|
|
7c7ed8bf55 |
MUL-5937 fix(daemon): persist direct chat Codex sessions (#6644)
Co-authored-by: 전영진 <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
93fa120c4c |
MUL-5925 fix(execenv): expand Windows ~\ in the openclaw active config path (#6630) (#6643)
`openclaw config file` shortens the home prefix with its host OS's separator, so Windows reports `~\.openclaw\openclaw.json`. The tilde expansion matched only the `~/` form, so the Windows path fell through unexpanded; not being absolute, it was then joined onto the daemon's working directory into a path that can never exist. The resulting stat miss is indistinguishable from a fresh install, so the per-task wrapper was written without the `$include` of the user's config and without an include root. Every task then booted OpenClaw with no model providers and no auth profiles, failing with `Unknown model: openai/gpt-5.5` or `No credentials found for profile ...`. Accept both separators regardless of GOOS so the Windows shape stays testable from the Linux/macOS test job, and log the discovery outcome: discovery was previously silent, which is why this could only be diagnosed by reading the generated file. A missing active config now warns that the task will not see the user's models or auth profiles. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
545d890dae |
feat(cli): add --compact to issue comment list, adopt it in every agent read command (MUL-5442) (#6546)
* feat(cli): add --compact to issue comment list, adopt it in every agent read command (MUL-5442) The #5999 follow-up its closing note asked for, now that the workspace measurements exist: the two bounded reads the agent workflow prescribes are dominated by reader-noise metadata, and every byte of it enters the conversation history at full price once and is replayed at cache-read price by every later call of the session. --compact (JSON output only, opt-in) drops per comment: the issue_id echoed from the request path, source_task_id, updated_at when identical to created_at, null-valued fields, and empty arrays. Content, identity, and thread-summary fields pass through untouched; a real edit timestamp or non-empty array is never dropped. Measured on MUL-5442 (production data): --roots-only --summary scan 6,117B -> 4,738B (-23%); --thread --tail 30 deep read 47,359B -> 39,349B (-17%). Adoption: all nine runnable comment-list command templates in the per-turn channel (prompt.go, reply_instructions.go) now carry --compact, and the brief's Available Commands line and workflow step 3 document it (+~40B one-time, byte-stable). Command pins updated accordingly; new unit test pins the drop/keep contract and the help-contract test pins the flag's rendered usage. Known local-env failure: TestResolveWorkspaceID_AgentContextSkipsConfig fails identically on clean origin/main in this agent runtime (agent context env interference) — unrelated. Co-authored-by: multica-agent <[email protected]> * test(cli): wire-level --compact coverage and zero-value keep pins (MUL-5442) Review nits on #6546: TestRunIssueCommentListCompactWiring drives runIssueCommentList against an httptest server both ways — default JSON keeps every field, --compact drops the reader-noise set while zero-value scalars (reply_count: 0, content_truncated: false, folded_count: 0) survive, pinning that null pruning never generalizes to zero pruning. Same zero-value pins added to the pure-function test. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
2f5efa34ca |
test(daemon): add fan-out guard anchors for --content-file and inline --content (MUL-5825) (#6544)
Elon's #6517 review nit: the block-level banned list omitted the old cookbook's two central mechanism anchors, so a prose-only restatement of the flag mechanics ("use --content-file / don't use inline --content") could regrow in the fan-out block under green tests. Add both anchors, and replace the phrasing-fragile "Do NOT write literal" anchor with the semantic backtick-\n-escape anchor that survives rewording. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
73ab498d1d |
MUL-5811: fix(squad) derive leader role from wire fields, not instructions text (#6519)
* fix(squad): derive leader role from wire fields, not instructions text (MUL-5811) The daemon decided whether a task ran in the squad-leader role by grepping the agent's Instructions for the "## Squad Operating Protocol" heading that the server-injected briefing starts with. Role detection therefore depended on user-writable Markdown: any ordinary agent whose own instructions happened to contain that heading was promoted to leader and handed the leader rules — mandatory `multica squad activity` calls, a licence to exit silently on no_action, and the squad-maintenance CLI section in its brief. The correct signals were already on the wire the whole time. The claim handler decides injection from explicit fields (`task.is_leader_task` + `squad_id` for issue-bound runs, the quick-create `squad_id` for the picker path) and forwards both to the daemon. Read those instead: - `taskIsSquadLeader` returns `IsLeaderTask || SquadID != ""`; brief assembly now shares that helper instead of repeating the substring check. - The claim handler clears `is_leader_task` on the response whenever its defensive gate withholds the briefing (NULL squad_id, squad hard-deleted, leader swapped after enqueue). The flag now means "briefing injected", so a leader task with no roster and no protocol degrades to an ordinary agent turn instead of booting into a leader role it cannot perform. Every legal path is byte-identical: the server sets these fields on exactly the claims it injects into, so real leader runs and ordinary runs produce the same brief and prompt as before. The only behavior change is the misdetection case, which is the bug. Also corrects two comments claiming IsSquadLeader is agent configuration rather than a per-task role, and notes the flag contract in the squad source map. Co-authored-by: multica-agent <[email protected]> * fix(squad): gate leader-role field reads behind a server capability (MUL-5811) Review catch (#6519): reading is_leader_task / squad_id unconditionally breaks the new-daemon → old-server direction the repo deliberately supports (claimTasksLegacy, MUL-4257). is_leader_task only reached the claim response in #4951, while briefing injection predates it — so against an older server a real issue-bound leader task arrives with the full briefing and both fields at zero, and field-only detection would silently demote that leader to a plain worker, dropping the entire operating protocol. Add `leader_role_resolved`, a claim-only wire capability the server sets on every claim response (leader or not). Its absence is what identifies a server too old to have answered the role question: - capability present → the explicit fields decide, and an ordinary agent whose instructions contain the briefing heading is no longer promoted to leader; - capability absent → keep the legacy briefing-marker inference, which is exactly today's behavior against those servers. That branch goes away once a minimum server version is enforced. The field is claim-only and never rendered into a prompt, so it costs no prompt tokens and changes no brief or prompt bytes on any path. Tests: the role table now covers both server generations, including legacy real-leader (heading, no capability), current ordinary-agent-with-heading, and current withheld-briefing; an end-to-end legacy prompt test pins that an old server's leader keeps its rules; and every claim test now asserts the server actually advertises the capability — losing it would send all daemons back to text inference. Also rewords two comments per review: the brief byte-stability tradeoff is owner-accepted and recorded in MUL-5811, not an open tracking item. Co-authored-by: multica-agent <[email protected]> * docs(squad): state the pre-capability server shapes precisely (MUL-5811) Review nit: the comments said every server without leader_role_resolved "never sent is_leader_task", which is only true before #4951. Servers between #4951 and this change do send the flag — they just never promised it implies an injected briefing, which is the actual reason the fallback covers them too. Names both shapes at each site (daemon types, handler response, helper doc, role-table doc, squad source map) so a future cleanup of the fallback reads the version window correctly. Comments only; no behavior change. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
6ca6b27ce3 |
refactor(daemon): converge multi-thread fan-out cookbook into Comment Formatting pointer (MUL-5825) (#6517)
The cross-thread reply fan-out block embedded a full file-operations cookbook (body file -> --content-file -> cleanup, the inline/stdin bans, the \n-escape rule, plus two example command pairs) that triple-wrote the mechanism already carried by the brief's ## Comment Formatting and the single-thread cookbook. The block now keeps only what is multi-thread-specific — fan-out announcement + override, posting order, per-thread --parent targets, the DISTINCT-body-file-per-thread delta, and the squad leader's whole-block no_action scope sentence — and points at ## Comment Formatting for the mechanism. Dropping the embedded commands removes the only OS-dependent text, so the windows/unix cookbook variants collapse and the block no longer branches on runtimeGOOS; the OS split lives solely in the brief. Measured (2-thread, 36-char-UUID fixture): ordinary 1,657B (linux) / 1,675B (windows) -> 919B both; leader 1,870B / 1,888B -> 1,132B both. ~-740-760B per coalesced multi-thread turn. Pins updated per discipline: ordinary unconditional form and the leader scope-ordering assertions are retained (obligation strings track the new text, ledger comments record the mapping); retired cookbook strings get negative guards in the block test and the assembled-prompt test; a new non-parallel test pins OS-invariance. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d6d1996210 |
refactor(daemon): retire the generic no-reply rule — ordinary agents always post one comment (MUL-5442) (#6493)
* refactor(daemon): retire the generic no-reply rule — ordinary agents always post one comment (MUL-5442) Owner decision (MUL-5442, 2026-08-06): the no-reply / silent-exit behavior is squad-leader-only by design. Mechanically it always was — computeCommentAgentTriggers routes agent-authored comments only via explicit @mentions (the sole implicit wake is the squad-leader path), so loop prevention lives in the Mentions discipline, and the generic rule only suppressed one terminal noise comment on an already-enqueued run while giving every ordinary agent a legal silent-exit path that risks silently dropped results. - prompt.go: delete the per-turn agent-trigger warning block entirely (−728B per agent-triggered turn; supersedes #6484's compression). - runtime_config_sections.go: delete the Reply-mode reply-warranted bullet and step 5's conditional pointer; the reply bullets become unconditional (−472B one-time in the brief). Squad-leader no_action bullets and the Mentions section are untouched. - reply_instructions.go: 'If you decide to reply, post it' → 'Post your reply' (−16B per Reply turn); the leader's no_action block states its own exception. - Tests: retired pins are converted to negative guards with ledger comments (brief and per-turn both must NOT carry the retired apparatus); attribution, member-leak, and no-MUST-respond guards stay. Co-authored-by: multica-agent <[email protected]> * fix(daemon): scope the reply imperative with the leader's no_action carve-out, close the pin-ledger gaps (MUL-5442) Review catches by Elon on #6493, both accepted: 1. The leader's only silent path was contradicted later in the same prompt: the no_action rule said EXIT without commenting, then the cookbook and the brief's mandatory bullet said post unconditionally. Both now branch on squad leadership — agent configuration, never per-run state, so each agent's brief stays byte-identical across turns and issues (cache-safe): leaders get 'Unless your outcome is no_action, post your reply…' in the cookbook and 'Unless your outcome is no_action (Squad leader rule above), posting your reply as a comment is mandatory' in the brief; ordinary agents keep the unconditional wording byte-for-byte. New tests render the COMPLETE leader prompt and leader brief and assert the carve-out is present AND the unconditional capital-P imperative is absent — the scope relation, not two isolated Contains. 2. The retirement ledger named five brief pins but guarded only two, and the per-turn 'If you decide to reply' had no guard. Added the four missing negative guards in ordinary-agent scope (the leader's 'DO NOT post any comment' matches none of them) plus a leader-leak guard on the ordinary brief. Co-authored-by: multica-agent <[email protected]> * fix(daemon): scope the multi-thread fan-out under the leader's no_action carve-out (MUL-5442) Ports the per-turn half of #6493 review rounds 2-3 into the slimmed PR (the brief role-independence refactor moves to its own sub-issue/PR per owner decision): BuildMultiThreadCommentReplyInstructions takes the leader flag, and the leader lead sentence scopes the ENTIRE fan-out block — every later obligation sits under the 'Otherwise' — with index-order test guards. Ordinary fan-out output is byte-unchanged. Also repairs a truncated TestFreshSessionMayHelp from the earlier rebase conflict resolution (missing closing braces). Co-authored-by: multica-agent <[email protected]> * docs: correct the taskIsSquadLeader comment — leadership is a per-task role (MUL-5442) Review nit on #6493: the helper's comment still claimed leadership is 'agent configuration, never per-run state' — the disproven premise that drove the earlier review rounds. State the per-task-role fact and point at MUL-5811 for the role-independence work. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
62a2d70afd |
refactor(daemon): stage-2 — Mentions and Comment Formatting judgment rewrite (MUL-5442) (#6453)
* refactor(daemon): rewrite Mentions and Comment Formatting to their judgment form (MUL-5442) Stage 2, final two sections, bundled per the small-sections rule. Mentions: the four-link side-effect table stays verbatim (platform facts); the two H3 subsections merge into one policy paragraph keeping every anti-loop anchor — the no-mention default with its cost mechanism, the no-sign-off-mention ban, the end-with-no-mention rule, the three mention-warranted cases, and the silence closer. The retired headings' pins re-anchor to the policy phrases. Comment Formatting: both variants keep the full operational contract (file-first sentence verbatim, both bans with incident ids, workdir scope, --parent continuity, cleanup, newline rule); what goes is the mechanism narration (what the shell rewrites, how flags get swallowed, PowerShell version/encoding detail — the consequence stays). Co-authored-by: multica-agent <[email protected]> * fix(daemon): correct the PowerShell version claim, pin the mention scope qualifiers, section-scope the formatting assertions (MUL-5442) Review catches by Elon on #6453, all three accepted: 1. The compressed Windows rationale over-generalized a version-specific fact: '$OutputEncoding drops non-ASCII' is true of Windows PowerShell 5.1 (ASCII default), false of PowerShell 6+ (utf8NoBOM). Now reads 'Windows PowerShell 5.1 ... may replace non-ASCII characters with ?'; the Go comment documents the version split and why file-first stays version-agnostic (agents cannot rely on which shell services the pipe). 2. The merged Mentions paragraph was pinned only at the list head — the scope qualifiers ARE the anti-repeat-notify boundary: 'not yet involved', 'for the first time', 'explicitly asks to loop someone in', and the loop-cost mechanism are each pinned individually now. 3. The Comment Formatting assertions ran against the whole file, where '#4182' also appears in Available Commands — the HEREDOC ban could vanish with green tests. The assertions now slice the section (matched at the line-start heading, since Available Commands references the heading inline) and cover all seven contract elements within it. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
f44df04f96 |
refactor(daemon): rewrite the Output section to its judgment form (MUL-5442) (#6425)
Stage 2 section 2. The delivery contract keeps every platform fact and boundary: comment-add as the only delivery channel, the invisible terminal, exactly one comment per run before turn exit, --attachment as the only file path, the runtime-local-path ban with the code-location form and the say-so-in-words fallback. What goes is derivation: the invisible-task consequence restatement, the plans-in-your-reasoning elaboration, the good/bad style examples, the exists-right-now clause, and two quick-create explanation tails. One pin re-anchor: 'Do not assume any workspace issue prefix' follows the rewrite to 'never assume a workspace issue prefix'. Every other Output and delivery-invariant pin passes unchanged, including the MUL-4899 trio and the anti-dangling pointer target the Attachments section names. Issue-kind Output: 1,493 -> 978. Brief (real-UUID fixture): 13,382 -> 12,907 (-475); quick-create variant sheds ~160 more on its own kind. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c896f677d5 |
fix(agent): recover from a codex resume that overflows the reader (MUL-5722) (#6420)
Layer 1 (#6383) raised the shared scanner cap to 32 MiB, which moved the cliff without removing it — a codex rollout is append-only, so a thread that outgrows any fixed cap still fails its resume forever. This adds the recovery path. Layer 2: an oversized thread/resume response is reported as Result.ResumeRejected rather than a crash, which is the positive evidence shouldRetryWithFreshSession needs and reconnects the recovery #5715 had unintentionally cut off. Reaching it required one real fix — the reader wrapped the scanner error with %v, so bufio.ErrTooLong never survived to a caller. Layer 3: a new codex_resume_oversized reason marks the session resume-unsafe, and both resume lookups block by TIME rather than by matching the failed row. That shape is required, not stylistic: the failure happens before the turn starts, so the row lands with session_id NULL and is dropped by latest_per_session before any error-text filter runs. The block expires once a thread terminates after the overflow, so an issue recovers instead of starting cold forever. Also splits the MUL-4424 continuity notice by what each surface can still read — issue comments and Slack channel history can be re-read, web chat and Feishu cannot — so only the last group tells the user a loss happened. The backend no longer holds any of that wording; it receives ExecOptions.ResumeContinuityNotice from the caller and stays silent when the prompt already carries it, which makes the duplicate injection on the retry path structurally impossible. Known gap, tracked not claimed: for chat, the claim handler reads chat_session.session_id before the fallback query, so a daemon predating this PR leaves that pointer naming the oversized thread. Clearing it needs the attempted session recorded at claim time, which is a schema change. |
||
|
|
86f255658f |
MUL-5730 feat(server): expose chat audience without bloating prompts (#6428)
Tell a chat agent whether it is answering in a shared room or a 1:1, without splitting the cached brief prefix: the room shape rides the existing channel binding row to the claim response, and is stated once per turn in the chat prompt rather than in the static runtime brief. Originally contributed as #6390; re-rolled to move the audience fact to the per-turn prompt and to minimize the copy it renders. Co-authored-by: Seacen Zhao <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
2bb13667c6 |
refactor(daemon): cross-channel command dedup — brief states the loop, per-turn carries the commands (MUL-5442) (#6421)
* refactor(daemon): move the workflow's command templates to the per-turn channel (MUL-5442) Cross-channel dedup, brief side. MUL-5721 measured that every issue-turn variant of the per-turn message already carries the ready-to-run commands with real ids (issue-get line, reading hints, reply cookbook), fresher than the brief's static copies. The six workflow steps now state the loop shape — command names and flag mnemonics only; full templates and every embedded issue UUID leave the brief. The Ownership status commands and the squad-activity call switch to <issue-id> placeholder form. Doctrine pins stay on the brief side (mandatory catch-up, scan-first order, both motivation anchors); command-template pins re-anchor to names and mnemonics. Two design-consequence test updates: the static-catch-up assertion repoints at the doctrine (the full command now lives in every per-turn variant), and the byte-identity non-vacuity guard varies agent identity instead of issue id — because the brief is now deliberately issue-id-independent, which opens the cross-issue prefix-cache door. Co-authored-by: multica-agent <[email protected]> * refactor(daemon): point the reply cookbook at Comment Formatting instead of restating it (MUL-5442) Cross-channel dedup, per-turn side. The cookbook's prose restated the brief's Comment Formatting rules (file-first rationale, inline-content and HEREDOC hazards, the full Windows $OutputEncoding mechanics) around the command that already demonstrates the shape. It now keeps the file-first order, the ready-to-run command, the literal-newline rule, and the pointer; the hazard mechanics live once, in the brief section the pointer names. Default cookbook: 639 -> 494 bytes (-145 per reply turn, uncacheable channel); the Windows variant sheds ~200 more. Pins re-anchor from the deleted prose to the surviving anchors (file-first order, the pointer, the command form); the MUL-2904/#4182 banned-shape negative guards are untouched. Co-authored-by: multica-agent <[email protected]> * fix(daemon): narrow the per-turn coverage claim, test the cross-issue invariant, strip the command shape from the guest-visible prohibition (MUL-5442) Review catches by Elon on #6421 plus the CI failure, all three related: 1. The steps header overstated what the per-turn message carries — it ships the issue id and ready-to-run context-read commands; other calls are assembled from Available Commands. Reworded to say exactly that (a factual claim in a prompt cannot rely on the reader discovering it is wrong). 2. The cross-issue byte-equality this PR claims as a design benefit is now asserted directly per provider (same stable inputs, different 36-char issue UUIDs, byte-identical brief) — a Contains-based negative cannot catch truncated/transformed/conditional id use. 3. CI: the squad guest-leader contract test bans any runnable 'issue status <issue-id> in_review' shape in guest-visible text; the placeholder rewrite made the leader dispatch rule's NEGATIVE sentence match that ban. The prohibition now states itself without a command form ('do NOT move it to in_review or done on this turn') — negative sentences should not carry copy-pasteable command shapes at all. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
b98f760f1f |
MUL-5604: 支持 Reasonix runtime (#6370)
* feat(agent): add Reasonix runtime Co-authored-by: multica-agent <[email protected]> * fix(agent): make Reasonix sandbox host-adaptive Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Eve <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c3aab05a38 |
refactor(daemon): rewrite Background Task Safety to its judgment form (MUL-5442) (#6381)
* refactor(daemon): rewrite Background Task Safety to its judgment form (MUL-5442) Owner-authorized judgment rewrite per the Claude 5 context-engineering guidance: state the platform fact and the boundaries an agent cannot infer, drop the enforcement details a frontier model derives. The section is now three paragraphs: (1) the fact everything derives from — turn exit is task-terminal, no wakeup exists, never background-and-yield — with the foreground-collect, synchronous-fallback and no-standing-by consequences stated once; (2) the external-systems/CI boundary: not run-owned, the named --watch ban (kept because MUL-5223 proved the principle alone did not stop CI-watching), merge-gate is not acceptance criteria, the hand-off phrasing, and the single explicit-ask exception; (3) the persistent-service handoff contract, review-locked verbatim. Deliberately dropped as derivable: the run-owned work enumeration, the tool-promise enumeration, the wait/collect split rule, the persistent-service scope bullet, auto-merge and snapshot elaborations. Both pin lists rewritten to the surviving semantic anchors (21 each); retired pins are listed in the test comments with the rationale. 2,980 -> 1,931 (-1,049). Incident history stays in the Go comment where it costs no context. Co-authored-by: multica-agent <[email protected]> * fix(daemon): delete the duplicated CI exception, pin the full ban clause, sync the comment ledger (MUL-5442) Review catches by Elon on the judgment rewrite, all three accepted: 1. The old exception bullet survived below the handoff paragraph — the CI exception appeared twice, the second copy scope-widened by its position, and every substring pin stayed green. Deleted; both BTS tests now count-guard exactly one 'The one exception' occurrence. 2. The compound watch/poll ban was pinned only by its first command; the full clause is the MUL-5223 boundary, so the pin now covers all three members. 3. The Go comment ledger still described the pre-rewrite bullet model (retired pins as kept, a forward reference that no longer exists). Rewritten to the three-paragraph model so the safety-decision history reads true. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
918803f9a4 |
fix(execenv): preserve colliding QwenPaw skills (#6396)
Co-authored-by: multica-agent <[email protected]> |
||
|
|
18f8f78af5 |
refactor(daemon): per-turn message slimming — OPT-1 UUID dedup + OPT-2 initiator prose (MUL-5721) (#6382)
* refactor(daemon): drop duplicate UUID commands from per-turn comment hints (MUL-5721) OPT-1 from the MUL-5721 measurement: within one hint, a second full `multica issue comment list <uuid> ...` command restated the issue UUID (and, in the resumed hint, both anchors) for no routing value. The warm hint's issue-wide catch-up and the cold hint's roots scan become flag swaps on the thread command they follow; the resumed hint loses its anchor-restating sentence (the read command carries the thread anchor, the reply cookbook carries the trigger id as --parent). Fixture-measured: warm hint 555->481, resumed 551->417, cold 429->396; reply-turn totals -74/-134/-33 B. Ownership prompt and the reply cookbook (with its concrete --parent command) are untouched. Every dropped duplicate gets a deny assertion so it cannot silently return; the demoted fallbacks get semantic anchors in its place. Co-authored-by: multica-agent <[email protected]> * refactor(daemon): compress Task Initiator attribution prose (MUL-5721) OPT-2: the block's 400 B attribution paragraph carried two derivable restatements ("in a workspace many people can reach", "so this attribution does not widen what you can read or write"). The compressed paragraph keeps every rule: per-person privacy/access rules apply, the initiator (not the runtime owner) is who you are answering, credentials stay scoped to the runtime owner, and the initiator may not see everything you can. Both MUL-2645 test-pinned phrases survive verbatim; the fact line and heading are untouched. Fixture-measured: block 513->370 B (member+email form). The two previously unpinned surviving rules gain anchors ("is who you are answering", "do not assume the initiator can see everything you can"). Co-authored-by: multica-agent <[email protected]> * refactor(daemon): restore initiator read/write authorization boundary (MUL-5721) Review must-fix (#6382): the compression treated "attribution does not widen what you can read or write" as derivable from credential scoping and dropped it. It is an independent boundary: credential scoping names whose credentials run, the visibility sentence constrains disclosure — neither stops the model from treating an initiator's request as extra authorization. Restored as a short explicit negation with a semantic anchor. Initiator block 370 -> 440 B (still -73 vs the original 513); typical reply turn 2,191 B (net -147 for the PR). Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
6b5d26b235 |
feat: add QwenPaw ACP backend support (MUL-5355) (#5986)
* 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 #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 #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 #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 #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 <[email protected]> --------- Co-authored-by: niudakok <[email protected]> Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
1e661bb04b |
refactor(daemon): stage-1 judgment rewrite — mode router and small guardrail sections (MUL-5442) (#6365)
* refactor(daemon): compress the mode router and Reply-block preamble to their facts (MUL-5442) Stage 1 batch A of the judgment rewrite. The mode-router paragraph spent ~800 bytes explaining WHY the file cannot know the turn mode; the fact an agent needs is only WHERE the mode is named and that exactly one mode block applies. Compressed to a single Turn-mode lead carrying every routing fact: both mode lines, the shared-steps note, the one-block rule, and the no-mode-line default. The Reply block's first bullet drops its derivations (session history, confusion warnings) and keeps the id-source rule; the reply-warranted bullet keeps both pinned anchors and every behavioural outcome, dropping only the worked examples. Re-pins: the 'Mode router' heading anchor becomes '**Turn mode.**'. Co-authored-by: multica-agent <[email protected]> * refactor(daemon): trim the mechanism prose from the small guardrail sections (MUL-5442) Stage 1 batch B. Comment Formatting (both variants), Issue Body Formatting, and Always Use the CLI keep every ban, every incident id, and every pinned phrase verbatim; what goes is the mechanism narration (what the shell rewrites, why the CLI rejects outside paths, the resource-type enumeration). Zero test changes — every pin survives as-is. Co-authored-by: multica-agent <[email protected]> * fix(daemon): restore two platform boundaries and pin the routing rules (MUL-5442) Stage-1 review catches by Elon on #6365: 1. Two behavioural boundaries had been deleted as explanation. The issue-body H1 rule narrowed 'body or description' to 'body' — but 'description' is the CLI/API field name, so the alias is a cross-surface mapping. And step 2 lost 'CLI failures are normal' — platform failure semantics (a failed metadata read must not block the task), not tool mechanics. The second deletion also went undisclosed in the PR description. Both restored; both now pinned. 2. The compressed Turn-mode lead was guarded only by its markers: every routing rule (shared steps, apply-exactly-one, status difference, no-mode-line fallback, no status change) could have been dropped with green tests. All five now pinned as semantic anchors, and both Reply outcomes (work-produced arm, silent-exit arm) pinned individually alongside the existing heading anchors. Co-authored-by: multica-agent <[email protected]> * fix(daemon): pin the full no-mode-line mapping and gofmt the touched tests (MUL-5442) Final-review catches on #6365: runtime_config_kind_test.go:99 was not gofmt-clean (a scripted insertion), and the no-mode-line fallback was pinned as two separable halves — 'No mode line' plus a 'Reply mode' that other file content satisfies — so the text could mutate to route the fallback at Ownership mode with green tests. The anchor is now the exact mapping clause 'No mode line → Reply mode'; the status anchor stays. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
84fe84cd9e |
fix(daemon): resolve six injected-instruction contradictions (MUL-5696 conflict audit) (#6354)
* fix(daemon): resolve six injected-instruction contradictions found by the MUL-5696 conflict audit Cross-checking the four injection surfaces (runtime brief, per-turn prompts, built-in skills, CLI help) against each other: - brief taught --due-date <RFC3339> while the CLI help, the projects skill, and util.ParseCalendarDate all pin YYYY-MM-DD (RFC3339 passes only at exact UTC midnight); 3 occurrences aligned to <YYYY-MM-DD> - autopilot per-turn prompt banned 'multica issue get' unconditionally while the brief's autopilot workflow allows issue commands when the autopilot instructions direct issue work; prompt now mirrors the brief - quick-create per-turn prompt said 'do NOT pass --attachment' while the quick-create ## Output section (test-pinned) names --attachment on the create call as the surface's only file channel; ban is now scoped to URLs from user input - assignment prompt claimed the workflow file documents pagination, but MUL-5442 (#6347) retreated those semantics to --help; pointer updated - 'attachment download' help example recommended -o /tmp/images, the exact machine-shared path the --content-file/--attachment workdir guards reject (MUL-4252) and the brief's Attachments section assumes away; example moved inside the workdir - multica-squads skill still listed an unbounded 'issue comment list' pull, contradicting the never-one-bulk-pull doctrine (same class as MUL-5372 / #6347); replaced with the bounded roots scan - multica-working-on-issues pinned a different metadata write bar ('explicit task requirement') than the brief's two-condition bar; aligned to the brief MUL-5696 Co-authored-by: multica-agent <[email protected]> * fix(cli): create the download output directory; pin MUL-5696 alignments with tests Review follow-ups for PR #6354 (MUL-5696): - 'attachment download -o' now creates the output directory, so the help example ('-o ./attachments') works from a clean workdir; regression test added and verified red without the fix - the run-only autopilot issue-command boundary is now a single shared constant (execenv.AutopilotIssueCommandsGuard) emitted by both the brief and the per-turn prompt, so the two copies cannot drift apart again - regression pins for the audit alignments: the brief --due-date synopsis stays calendar-day (<RFC3339> banned), the quick-create prompt keeps the URL-scoped --attachment boundary (blanket ban banned), the squads skill carries no unbounded comment read, and working-on-issues keeps the brief's metadata write bar Co-authored-by: multica-agent <[email protected]> * test(cli): let attachment download tests run inside agent workdirs Second-round review follow-up for PR #6354 (MUL-5696): the download tests set only setCLITestServerEnv's non-mat_ token, so inside an agent workdir (daemon task marker present) newAPIClient rejected them before the download logic ran — including the new directory-creation test, which made it a net-new in-tree failure. All three download tests now set a task-scoped mat_ test token, the same pattern the upload tests already use; in a standard agent workdir the full-suite failure set drops from 94 (base) to 92, with the two pre-existing download failures fixed and none added. Also bans the old 'explicit task requirement' metadata phrase in the working-on-issues skill test (review nit). Co-authored-by: multica-agent <[email protected]> * refactor(daemon): single-source the autopilot guard in the brief; trim per-turn prompt additions Follow-up to keep PR #6354 byte-neutral-or-better per surface: - the per-turn autopilot prompt no longer restates the issue-command boundary; the brief's autopilot workflow section is its single emission point (a second hand-maintained copy is exactly how the two drifted before), pinned on both sides - the assignment prompt's read-surface pointer and the quick-create attachment bullet are tightened Measured against origin/main with identical contexts: per-turn prompts assignment -26 B, autopilot -68 B, quick-create -7 B; briefs +6/+6/+3 B (YYYY-MM-DD is 3 bytes longer than RFC3339). Net per run: assignment -20 B, autopilot -62 B, quick-create -4 B, comment-reply +6 B. Rebase note: the audit's metadata-bar finding is superseded by #6351, which landed the opposite canonical direction (brief defers to the skill); the rebase keeps main's owner-ruled skill text and drops the now-wrong test pins. --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
dc60429366 |
refactor(daemon): slim Issue Metadata to its judgment core, defer discipline to the skill (MUL-5442) (#6351)
* refactor(daemon): fold the metadata ban list into the write rule (MUL-5442) The Issue Metadata section spent 1,024 bytes teaching a KV bag. The What-NOT-to-pin bullet was a separate heading restating the write rule's negative space; it folds into Write on exit as one sentence with every ban kept explicitly (secrets/tokens/API keys, logs or comment summaries, runtime bookkeeping, single-run details). The parenthetical examples and connective prose go; every rule stays. Pins updated in kind: the merged ban list is pinned as one sentence plus the result-comment redirect, so the next compression pass cannot drop a ban without failing CI. -202 bytes (16,099 -> 15,897 on the standard fixture). Co-authored-by: multica-agent <[email protected]> * fix(daemon): restore the runtime-bookkeeping examples that define the ban (MUL-5442) Review catch by Elon on #6351: 'runtime bookkeeping' has no other definition anywhere in the brief or skills, so its examples (attempts, run timestamps, agent IDs) are the category's boundary, not connective prose — and all three are high-probability miswrites that can look like re-readable diagnostics or durable facts. Restored inside the merged bullet. Also adopts the review's pin advice, which is our own anchor-pin doctrine applied properly: the whole-sentence pin from the previous commit is replaced with separate semantic anchors (each ban plus the bookkeeping examples plus the result-comment redirect), so the sentence can be reworded later without CI churn while dropping any single ban still fails. +52 bytes; the section lands at 874 (from 1,024), the PR nets -150. Co-authored-by: multica-agent <[email protected]> * refactor(daemon): defer the metadata write discipline to the working-on-issues skill (MUL-5442) Owner decision on MUL-5442: metadata is deliberately free-form custom key-value state — the recommended-keys block never matched the feature's intent and is removed outright, not relocated. The full ban list defers to the multica-working-on-issues skill, which already carried a near-complete copy; the skill gains the two bans it lacked (secrets/tokens/API keys, agent ids) so nothing loses its home. The brief keeps only what the interface cannot express: the read-as-hints stance, the will-a-future-run-re-read-it bar, and the two write-time boundaries (never secrets, never long content). Section: 874 -> 505 bytes (1,024 at the round's start). Pins move with the content: the brief side slims to the surviving semantics plus the skill pointer; the skill-side contract test gains the relocated ban anchors so the pointer cannot dangle. Co-authored-by: multica-agent <[email protected]> * fix(daemon): remove the curated key list from the skill and restore the ban categories (MUL-5442) Round-3 review catches by Elon on #6351, both accepted: 1. The recommended-keys concept survived in the skill — which loads exactly when an agent is about to write metadata, so it still steered free-form KV into a platform vocabulary. The owner's ruling was that the concept should not exist, not that it should move. The key list, the 'high-signal keys only' heading, and the pr_url-specific example are gone; the section now describes free-form durable custom state and a generic set example. A mustNotContain guard keeps the curation from creeping back. 2. The relocated ban list had dropped its two defining categories (runtime bookkeeping, other single-run details), leaving unlisted values looking writable. Restored with the reviewer's structure: each category names its examples, and the test pins every category AND every example as separate line-safe anchors — no item can be silently dropped again. Co-authored-by: multica-agent <[email protected]> * fix(daemon): remove the last key-vocabulary residue from the property-vs-metadata note (MUL-5442) Round-4 review catch by Elon on #6351: the property-vs-metadata bullet still read 'free-form scratchpad for run state (pr_url, waiting_on, ...)' — recommending the ruled-out fields AND calling metadata a home for run state, in direct conflict with the runtime-bookkeeping ban restored two sections above. Now reads 'free-form bag for durable custom issue state', consistent with both the owner ruling and the ban list. Test side per the review: the broad pr_url anchor narrows to the full stale-warning phrase (the one sanctioned pr_url reference — a negative compatibility note, not a write recommendation), and the curation guard gains the removed residue phrases so neither the vocabulary nor the run-state framing can creep back. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
766c7a7fa2 |
refactor(daemon): retreat deep read semantics to --help, trim BTS prose (MUL-5442) (#6347)
* feat(cli): carry the comment-read contract in comment list --help (MUL-5442) The --recent flag help gains the MUL-5372 saturation semantics (N caps threads, not comments; every thread returns uncapped; small issues return the whole history) and a pointer at the bounded alternative. The --before help names the stderr cursor labels alongside the response header. This is the relocation target for the brief's deep read semantics: the contract follows the flag, and TestIssueCommentListHelpCarriesReadContract pins it here so the brief-side pointer cannot dangle. Co-authored-by: multica-agent <[email protected]> * refactor(daemon): retreat deep read semantics to --help and trim BTS prose (MUL-5442) Unblocked by #6309: the platform no longer recommends --recent anywhere, so the flag's deep semantics no longer need to ride in every brief. - Available Commands comment-list entry: keep the signature, the two bounded read shapes, and the one-clause saturation warning; per-thread cap detail, folding rules, and cursor labels retreat to --help (pinned there by TestIssueCommentListHelpCarriesReadContract). - Workflow step 3: drop the worked examples and the redundant trailing pointer; the mandatory two-step read and both motivation pins stay. - Background Task Safety: tighten connective prose only — every pinned phrase and every behavioural constraint is untouched. - multica-squads SKILL.md quick-start: replace the last remaining --recent recommendation on the platform with the bounded scan — same class of fix as #6309, found while relocating the warning. -676 bytes on the standard fixture (16,766 -> 16,090). Co-authored-by: multica-agent <[email protected]> * fix(cli,daemon): repair the help rendering, restore the BTS orphan scope, complete the squads read (MUL-5442) Three review catches by Elon on #6347: 1. The --before usage string wrapped the cursor labels in backticks, and pflag's UnquoteUsage hijacked the first pair as the flag's value placeholder — rendered help showed '--before Next thread cursor' instead of '--before string' (same regression class as TestLoginTokenHelpOutputRendersCleanly). Labels now use double quotes, and the --recent help no longer suggests composing mutually exclusive flags ('--roots-only + --thread --tail' -> an explicit two-step read). TestIssueCommentListHelpCarriesReadContract now asserts on the RENDERED FlagUsages output, pinning '--before string' and the two-step order. 2. The compressed BTS opening said 'anything still running is orphaned', which swept externally-owned work (GitHub Actions) into the orphan rule the same section later scopes out. Restored: 'any run-owned work still active is orphaned', pinned. 3. The squads quick-start's roots-only scan never returns reply bodies, where mention triggers and failure reasons usually live. Added the bounded drill-down step and the sequence rationale; both reads pinned in TestSquadsSkillCoversLeaderRoutingContract, which also guards against a regression back to --recent. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
e92d828f0c |
refactor(daemon): compress brief prose and demote the sub-issue playbook (MUL-5442) (#6310)
* refactor(daemon): compress brief prose and demote the sub-issue playbook (MUL-5442) Six static edits to the issue brief, -1,249 bytes on the standard fixture (17,977 -> 16,728). All edits are identical for every issue run; prompt-cache byte stability (#6008) is untouched. - BTS persistent-service bullet: keep the handoff contract (deliverable-only, lifecycle detached, readiness verified, best-effort survival), drop the operational walkthrough. Pins re-anchored to concepts (durable logs, recorded PID, verify readiness). - BTS CI-ban bullet: keep the command blacklist and the complete-hand-off rule, drop the auto-merge/snapshot elaboration. - Ownership mode: state the identity-forbids clause once on the header instead of once per status bullet. - Delivery invariant: merge the three sub-bullets into the lead paragraph; also fixes the stale '(below)' — the per-surface delivery line renders above the invariant, not below it. - Sub-issue Creation: demote the todo/backlog/stage playbook to the multica-working-on-issues skill; the brief keeps a one-line flag map plus the skill pointer. Skill-side anchors added to TestWorkingOnIssuesSkillCoversIssueLoopContracts so the pointer cannot dangle. - Attachments: collapse the two-sentence CLI-fetch intro into one line. Every pinned behavioral phrase is either carried verbatim or re-pinned to an equivalent semantic anchor in the same assertion; no assertion is deleted without a replacement. Co-authored-by: multica-agent <[email protected]> * fix(daemon): restore the full persistent-service handoff contract (MUL-5442) Review catch by Elon on #6310: the compressed bullet weakened the contract two ways — the reply requirement dropped 'logs' from the URL/logs/stop triple (durable logs alone are unobservable if the user is never told where they are), and the general ownership/cleanup handle narrowed to a bare PID (a supervisor/profile-managed service has no single stable PID). Both test pin sets had been updated to the weakened phrases, which would have made the regression look legitimate. Keeps the compressed sentence shape; restores both halves of the contract and pins them ("cleanup handle such as PID/profile", "URL, logs, and stop instructions") so they cannot be compressed away again. +38 bytes; the PR still nets -1,211 against main. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3c6176cbcd |
refactor(daemon): de-duplicate cross-section rules in the runtime brief (MUL-5442) (#6302)
* refactor(daemon): fold four restatements of the end-of-turn rule into one (MUL-5442) Background Task Safety opened with four bullets that were four views of the same rule -- do not end the turn with run-owned work outstanding: the general ban, the "tool says it will notify you" case, the unobservable-result case, and the "standing by" sign-off. Separating them cost bytes without adding a distinct behaviour. Fold them into the leading bullet. Every phrase the behaviour tests pin is carried over verbatim, including "Do NOT end your turn while background tasks", "Never background-and-yield", "wait for a future notification/reminder", "running in the background so you can keep working", "run the work synchronously instead" and "standing by". MUL-5442 Co-authored-by: multica-agent <[email protected]> * refactor(daemon): demote workflow-step restatements of delivery, mention and metadata policy to pointers (MUL-5442) The six workflow steps and the Reply mode block restated policy that already has a dedicated section. Each restatement is a second place to edit when the policy changes, which is how they drifted apart in the first place. Give each rule one canonical home and leave a pointer at the call site: - delivery ("only a comment reaches the user") -> ## Output; steps 5 and the Reply block point at it. - mention discipline -> ## Mentions. The reply-time phrasing the loop-hardening test pins moves into that section rather than being duplicated in the Reply block, so the anti-loop signal is unchanged. - metadata read/write bar -> ## Issue Metadata; steps 2 and 6 point at it instead of paraphrasing the bar. Tests: the metadata scope test pinned the old pointer wording, and the mention test's comment claimed the sign-off rule lived in the workflow steps. Both are updated to the new placement; every behavioural phrase they guard is still asserted, file-wide. MUL-5442 Co-authored-by: multica-agent <[email protected]> * refactor(daemon): give the comment-read surface and the file-safety rules one home each (MUL-5442) Three cross-section duplications, each resolved toward the section that owns the rule: - comment reads: the workflow step and the Available Commands entry both explained what --roots-only / --summary / --thread --tail do. The step keeps the two reads it mandates and its anti-stale motive; flag semantics stay in Available Commands, which is the single discovery point. The saturation trap ("caps THREADS, not comments") and the pagination cursor labels stay put -- they are load-bearing after MUL-5372 and remain asserted. - --content-file: the comment add entry restated the guardrail Comment Formatting owns; it now names the rule and points there for the rationale. - workdir path rule (MUL-4252): issue create carried its own copy of the stale-file rationale; it keeps the rule and defers the why. - inbound attachments: trimmed to the pinned rule plus a pointer. MUL-5442 Co-authored-by: multica-agent <[email protected]> * refactor(daemon): merge the Agent Identity action list, gate squad maintenance, trim the attachment restatement (MUL-5442) Three follow-ups from review, each re-examined against what the text guards today rather than against the fact that a test pinned it. 1. Agent Identity enumeration. Instruction Precedence and workflow step 4 were added in the same commit (#3802) and each carried its own list of actions Agent Identity can forbid -- and the lists disagreed: one named status changes, the other named issue create/update and delegation, neither contained the other. Merge them into Instruction Precedence, which owns the rule. Step 4 keeps only what that section cannot express: a delegation-only role stops once its delegation is delivered. 2. Squad maintenance. `multica squad member set-role` shipped to every run, including every agent that leads no squad and therefore has no squad whose roles it could change. Gate it on IsSquadLeader -- agent configuration, not per-run state, so the brief stays byte-stable across runs of one session (MUL-5377), the same predicate the workflow already branches on. 3. Inbound attachments. The section restated Output's no-clickable-local-path rule verbatim. Keep the framing Output cannot express -- a downloaded attachment feels shared but landed in a private workdir -- and point at Output for the rule. The delivery test now also asserts the pointed-at rule is present, so the pointer cannot dangle. Brief size, plain issue task: 19,231 -> 18,009 bytes for an ordinary agent (-6.4%), 20,718 -> 19,759 for a squad leader (-4.6%). MUL-5442 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |