Files
multica/server/pkg/protocol
063698f571 fix: gate worktree mode on a declared capability, not a version string (MUL-5707) (#6904)
* fix: gate worktree mode on a declared capability, not a version string (MUL-5707)

Two tasks ran in the user's own directory despite the resource being set to
worktree mode. The daemon was v0.4.23 — built before the worktree code — so
it json-skipped execution_mode and took the in_place path. The claim gate
existed to stop exactly that and did not fire: the runtime reports
`v0.4.21-24-gcd3c0bb89`, a git-describe dev build, and the version floor
deliberately exempts those so `make daemon` stays unblocked. The exemption
is fine for a gate whose failure mode is a missing field; it is wrong for
one whose failure mode is writing to somebody's working copy.

Version strings cannot answer "does this binary implement worktree mode".
The daemon now answers it directly: `local-worktree-v1` joins the existing
X-Client-Capabilities negotiation, and all three gates read that instead.

- Claim time (authoritative): decided from the capability on that very
  request. A daemon without it gets the task cancelled with the reason on
  the row — never a silent fallback to editing the directory, which is the
  outcome this mode exists to prevent.
- Save time and the UI cannot see a live request, so registration now
  records the advertised capabilities on the runtime row and both read that.
  One signal everywhere instead of three different guesses.

Absent capabilities means an older daemon, so every path fails closed. The
error names the machine's directory and says to update the Multica app
there, rather than quoting version numbers the user did not choose.

MIN_LOCAL_WORKTREE_CLI_VERSION stays only as the number shown in the
existing 422 payload; nothing gates on it any more.

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

* fix(handler,views): judge daemon capability by the newest runtime row (MUL-5707)

Review follow-up. Both gates asked "did ANY runtime row for this daemon
advertise the capability", which never stops being true: deregistering a
runtime only flips the row to offline, its metadata survives, and
ListAgentRuntimes returns every row. A machine that once ran a capable
daemon and then downgraded keeps an old capable row next to the fresh
incapable one, so the save gate and the UI would go on offering worktree
mode. The claim gate still refused the task — no directory was ever at risk
— but the user only found out after the run instead of at the moment they
could act on it.

Both sides now read the daemon's most recently seen runtime row, and a row
missing the capability is never skipped: being newest is what makes it
authoritative, not whether its answer is convenient. A row that never
reported sorts oldest so a live one always wins. The selection lives in one
place per side (daemonAdvertisesWorktree / daemonSupportsLocalWorktree) with
matching semantics, because a UI that disagrees with the API offers a mode
the API refuses.

Also pins the bridge the whole gate rests on, which nothing covered:
DaemonRegister round-trips the advertised header onto the runtime row, and
re-registering WITHOUT the header clears it — the exact downgrade the save
gate has to notice. The daemon's own identity-header test now asserts
local-worktree-v1, so dropping it from the advertised list can never be a
silent change.

Corrects three places that still described the version floor as the gate:
version.go, the projects-and-resources source map, and the floor test, which
now pins the two surfaces that merely DISPLAY the version to quote the same
release.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-13 17:23:51 +08:00
..