mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
d276991c603e6041d20fb5c1e65df8756d35d2ae
945
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d276991c60 |
MUL-6743 refactor(issues): remove the assignment handoff note (#7626)
* refactor(issues): remove the assignment handoff note (MUL-6743) Usage data puts the handoff note at 0.2% of assignments, so the box occupied the confirm dialog's centre for the other 99.8% and bought them nothing. Removing it takes the whole feature with it: the note input and its runtime version gate, the `handoff_note` write field on PATCH/batch issue updates, the task column write, and the daemon's opening-prompt + issue_context.md handoff sections. The confirm dialog itself stays — it still names who is about to run and still offers "Don't start yet" (suppress_run). The `*WithHandoff` enqueue methods only ever varied by that note, so they become `EnqueueTaskForIssueByActor` / `EnqueueTaskForSquadLeaderByActor` — the actor-carrying variants autopilot already called with an empty note. `preview-trigger` drops `handoff_supported`, which nothing read after the gate moved client-side. `agent_task_queue.handoff_note` is deliberately left in place: it holds historical rows and this is the expand half of an expand-contract pair. Nothing reads or writes it now, so a follow-up migration can drop it once no deployed binary selects it. Co-authored-by: multica-agent <[email protected]> * docs(issues): reshoot the assignment confirm dialog (MUL-6743) The asset still showed the handoff note box the surrounding prose no longer describes. Recaptured from the app at the same 600x450 frame; the confirm button now carries the send-chord keycaps it has shipped with since MUL-5694. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
571e61128d |
MUL-7002: remove database reads from WebSocket heartbeats (#7976)
* fix(runtime): remove websocket heartbeat lookups Co-authored-by: multica-agent <[email protected]> * fix(runtime): preserve explicit deregistration Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d1c7ec25b1 |
feat(agents): add live model catalog refresh (#7919)
Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
731af7ccd6 |
MUL-7002: push runtime deletion invalidation to active daemons (#7969)
* fix(runtime): invalidate deleted active runtimes Co-authored-by: multica-agent <[email protected]> * fix(runtime): separate invalidation delivery metrics Co-authored-by: multica-agent <[email protected]> * fix(runtime): complete invalidation metric separation Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
0431218061 |
MUL-6833 fix(web): localize auth callback and error pages (#7752)
Localize the web auth callback, 404 page, and root error boundary, plus the shared section-level ErrorBoundary. Also fixes several pre-existing auth-path bugs surfaced during review: - access_denied was unreachable (the !code branch preceded the error-param check) - UserLocaleSync's reload could replay a consumed OAuth code and show 'Login Failed' after a successful sign-in - Google token-exchange and userinfo provider failures were misreported as user-actionable causes - checkSignupAllowed reported 'registration is disabled' for an allowlist miss |
||
|
|
879d0de916 |
refactor(agent tasks): drop the Active sibling runs prompt block (MUL-6984) (#7937)
Every issue turn carried a claim-time snapshot of the same agent's other in-flight tasks, scoped only by (agent, workspace) and capped at five. The scope had no relatedness filter, so the list answered "what else is this agent running right now", not "what else touches the code you are about to change". Measured over one day of runs on this workspace: the block was injected 78 times across 30 runs, listing 96 distinct (current issue, sibling) pairs, and not one pair shared a PR, a branch, or an issue family. `run-messages` was invoked zero times off it. Meanwhile the day's one genuine overlap — a PR review pushing to server/internal/metrics/pricing.go and a concurrent change to the model catalog next to it — was never paired, because overlapping in time is not the same as overlapping in code. The duplicate-run defect this shipped with (#6947) is fixed by shouldSuppressActiveSelfAssignment, which stops a second run on the same (issue, agent) at the server. Once that holds, the block can only ever list a DIFFERENT issue, so it was structurally unable to help with what it was added for. #7922 then gave the coordination question a correct answer — `issue runs --active` / `--siblings`, cross-agent, family-scoped, and read at the moment it is asked — which is the same information as a pull instead of a per-turn push. Removed end to end rather than hidden behind a flag: the prompt block, the claim payload field on both sides of the wire, and ListActiveSiblingIssueTasks. The field was `omitempty` and the daemon already rendered nothing for an empty list, so old and new server/daemon pairs stay compatible in both directions; nothing outside server/ ever read active_sibling_runs. Kept: the self-assignment suppression and its tests, `--no-start`, `issue run-messages`, and `issue runs --active/--siblings`. Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
603519d158 |
MUL-6977: test: remove implementation-only coverage (#7932)
* test: remove implementation-only coverage Co-authored-by: multica-agent <[email protected]> * test: remove remaining implementation-only coverage Co-authored-by: multica-agent <[email protected]> * test: restore compatibility and fragment navigation coverage Co-authored-by: multica-agent <[email protected]> * test: preserve behavioral cleanup contracts Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d4a712abf3 |
MUL-6771 feat(cli): filter and sort issue list by custom property (#7664)
Expose the `properties=` filter and `sort=property:<id>` that GET /api/issues already implemented but the CLI never surfaced, so scripts no longer have to page through everything and filter locally: multica issue list --property "Impact=High" --property "Impact=Medium" multica issue list --property "QA Status=__none__" --status in_review multica issue list --sort property:Impact --direction desc --property takes one Name=Value per flag; repeating a property ORs its values, different properties AND. Values resolve by name the way the rest of the property CLI works. __none__ is the reserved unset filter. A value the server could never have stored — a non-http(s) url, over-long text, a non-numeric number, a date in another format — is rejected up front rather than returning the empty page that reads like a real answer. The >=, <= and != spellings are reserved so comparison filters can be added later without breaking scripts. Also fixes propertySortExpr, which sorted a select property by the raw stored value — the option-id UUID, an arbitrary order. Values now rank by their position in the definition's option list, so an ordinal scale sorts by meaning (Low < Medium < High), matching what board grouping already treats as canonical. Closes #7663 |
||
|
|
765321f053 |
MUL-6951 fix(autopilot): carry the trigger creator's authorization on scheduled runs (#7918)
* fix(autopilot): carry the trigger owner's authorization on scheduled runs A schedule/webhook autopilot run left originator_user_id NULL, so canInvokeAgent saw no human and fail-closed against private agents. Arming a trigger is the same authorizing act as clicking "run now" — deferred, not absent — so withholding the originator did not remove that authority. It only forced every capability the automation legitimately needed to arrive as a separately-bounded borrow path: MUL-4857 for child creation, MUL-6691 for the run's own issues, #7902 for the accountable member's issues, with sub-issue dispatch queued up as the fourth. Each carries a hand-written scope that no operator can see in the UI. attribution.TriggerOwner / RuleOwner now populate UserID (the originator) instead of only the accountable side. finalizeAttribution mirrors it, so the originator == accountable invariant that migrations 190/197 enforce holds without a schema change or backfill. Both autopilot execution modes resolve through triggerOwnerAttribution, so run_only and create_issue are covered by the same two lines. This grants exactly the trigger owner's own rights — canInvokeAgent is untouched, only the principal handed to it changes — and originator_source still distinguishes a scheduled fire from a manual one in the audit trail. The borrowed-authority machinery in handler/agent_access.go is now dead for newly enqueued runs but is deliberately left in place: tasks enqueued by the previous backend are still running during a rolling deploy and carry originator NULL, and they must keep the path they were admitted under. Deleting it is a follow-up once this is deployed. Composio connected-app credentials also key on originator_user_id, so scheduled runs now receive the trigger owner's connected apps. Decoupling that from authorization was considered and explicitly deferred. Verification (real PostgreSQL 17): - internal/attribution, internal/service, internal/handler: PASS - the five attribution_stamp tests that pinned "originator stays NULL" were updated to assert the new principal, and fail against the old constructors - pre-existing, unrelated failures in internal/daemon/execenv (4 Hermes store-path tests) reproduce identically on a clean tree Co-authored-by: multica-agent <[email protected]> * refactor(autopilot): delete the borrowed-authority machinery With a scheduled run carrying its trigger owner as originator, every autopilot-shaped authority path in agent_access.go is unreachable: the invoke gate resolves a real human from the speaking task and judges it exactly as it judges that human acting directly. Removed: - autopilotTaskAssignAuthority + autopilotAssignAuthorityFromRequest (MUL-6691) - issueBoundToAutopilotTask, taskRunsAutopilotInWorkspace - autopilotDelegationAuthority and its FromRequest / FromComment resolvers (MUL-4857) - effectiveInvocationAuthorityFromRequest - assignAuthorityScope, assignScopeKind and the four scope constructors - commentTriggerComputeOptions.AutopilotDelegationAuthorityUserID + effectiveInvoker() validateAssigneePair and triggerTasksForComment lose their now-meaningless scope / delegation-authority parameters; every caller passes the run's originator instead. This also retires the fourth borrow scope #7902 proposed. CreateIssue still loads the parent issue when assigning, even though the gate no longer needs the row: it is what turns a cross-workspace or missing parent into 400 "parent issue not found" instead of a 403. Covered by a test. Two behaviour notes, both accepted: - Authority now propagates down a delegation chain, because the parent run finally has an originator to copy. Under the old design each hop had to re-borrow. This matches what a manual "run now" already did. - The old borrow path required task.Status == "running"; the originator path does not, and never did for any non-autopilot run. Task tokens are revoked at the terminal transition (TaskService, MUL-2600), so the primary guard remains; this only drops a second layer that existed solely for autopilot runs. Tests: the two suites that pinned the borrow bounds (autopilot-created-issue binding, authored-by-this-task binding) described a mechanism that no longer exists, so they collapse into one file asserting the rule that replaced them — rights / no rights / no human at all, across create-top-level, create-child, update and batch. The mention-authority suite keeps its shape; its fixture now gives the run a human, as a live run has. Verification (real PostgreSQL 17): internal/attribution, internal/service and internal/handler all PASS. Pre-existing unrelated failures in cmd/multica, internal/cli, internal/daemon and internal/daemon/execenv reproduce identically on a clean tree. Co-authored-by: multica-agent <[email protected]> * fix(autopilot): resolve one principal per dispatch and expire it with the run Addresses Elon's review of #7918. Three findings, all confirmed against the code before fixing. P1-1 — admission and the run's identity could be two different people. Automatic dispatch admitted the assignee agent as the AUTOPILOT CREATOR (canCreatorInvokeAgent) while the task took its originator from the trigger. With A owning both the autopilot and a private agent, and B owning the trigger, a dispatch was admitted as A and then executed as B — rights neither holds alone. The old suite could not see it because it called dispatchRunOnly directly, skipping admission. - autopilot_trigger gains an IMMUTABLE created_by (migration 449, best-effort backfill from published_by). published_by keeps its audit meaning and still transfers on a substantive edit; it is no longer an authorization input, because letting an editor move it silently re-authorizes the automation as that editor. Per Bohan's ruling the run always acts as the trigger CREATOR. - ResolveAutopilotTriggerPrincipal is now the single answer to "who does this run act as". Admission, the task stamp and every delegated run read it, so the fork is unrepresentable. It also binds the trigger to its autopilot (GetAutopilotTriggerForAutopilot — the old lookup was WHERE id = $1, which would let a foreign trigger select the principal) and re-checks workspace membership on every dispatch, so removing a member revokes their triggers. - canCreatorInvokeAgent is now unreachable and deleted. - RuleOwner reverts to accountable-only. It is a guess at who owns a rule; promoting it would hand a legacy trigger somebody's rights without that person arming anything. Such runs carry no originator and fail closed. P1-2 — a terminal task could still spend its human's rights. invokeOriginatorFromRequest now refuses completed / failed / cancelled tasks. Token revocation at the terminal transition stays the primary guard, but it is best-effort, explicitly non-fatal on failure, with a 24h expiry behind it. The check sits at the authorization boundary rather than the auth boundary, so a finishing run can still post its final comment but cannot start new work. P1-3 — the PR and code comments described Composio backwards. BuildTaskOverlay ignores the originator entirely and builds from the AGENT OWNER's connections and allow-list (MUL-3963). Naming the originator hands out no third-party credentials. Comments corrected; no decoupling was ever needed. Operational note: a trigger with neither created_by nor published_by (predating migration 186) resolves no principal, so its autopilot stops dispatching until the trigger is re-saved. That is the intended fail-closed behaviour. Tests: full dispatch through admission for both fork directions; principal fails closed on legacy / cross-autopilot / removed-member; terminal task refused on all three statuses; the edit-transfer tests now assert the principal STAYS with the creator while published_by moves. Verified the fork test fails when admission is reverted to canCreatorInvokeAgent. Verification (real PostgreSQL 17, pristine database): internal/attribution, internal/service, internal/handler and cmd/server all PASS. Migration 449 up/down round-trips. Pre-existing unrelated failures in cmd/multica, internal/cli, internal/daemon and internal/daemon/execenv are unchanged. Co-authored-by: multica-agent <[email protected]> * fix(autopilot): bind the trigger principal to its workspace, correct stale docs Second round of Elon's review on #7918. 1. The trigger lookup only enforced half of what its comment claimed. GetAutopilotTriggerForAutopilot matched on (trigger id, autopilot id); the workspace was used only for the creator's membership check afterwards. That check proves the resolved human belongs to the workspace PASSED IN, not that the autopilot does — a member of two workspaces satisfies it while the trigger came from the other tenant. The query now joins autopilot and filters on workspace_id, and the resolver passes it through. Regression test seeds two workspaces sharing one member and asserts the foreign workspace resolves nobody; verified it fails against the pre-fix query. 2. Several permission-boundary comments still described the pre-MUL-6951 model — that a run's authority and its accountable human transfer to whoever last edited the trigger. That is now the opposite of the code, and these are the comments a future reader would use to reason about the authorization model, so they are corrected at all four sites: the trigger create site, the SetAutopilotTriggerPublisher query, triggerOwnerAttribution, and the dispatchRunOnly attribution block. published_by is described as what it now is: a config-audit column with no bearing on a run. 3. The migration comment now states the backfill's imprecision plainly: for an already-edited trigger it freezes the last recoverable EDITOR as the immutable creator, not the historical creator, which the schema never recorded. It also records that a NULL/NULL trigger has no recovery path by decision (Bohan: leave them empty) — re-saving re-stamps published_by, not created_by — so its runs stay refused with a recorded failure_reason. Not done, per Bohan's ruling: no re-save takeover path for legacy triggers and no migration-period alerting. Verification (real PostgreSQL 17, pristine database): internal/attribution, internal/service, internal/handler and cmd/server PASS; full suite shows only the four pre-existing unrelated failures (cmd/multica, internal/cli, internal/daemon, internal/daemon/execenv). go vet ./internal/... and git diff --check clean. Co-authored-by: multica-agent <[email protected]> * docs(autopilot): carry the creator-principal semantics into every description Third round of Elon's review on #7918. Two must-fixes, no behaviour change. 1. The final semantics had not reached every surface that describes them. Migration 189 wrote COMMENT ON COLUMN text for autopilot_trigger.published_by_* saying it decides the accountable human of the runs a trigger fires. That is now false, and because sqlc generates column comments into models.go, the stalest description of the authorization model sat in generated code every reader passes through. 189 is released and untouched; migration 449 overrides both comments and documents created_by_* for the first time, and sqlc is regenerated. 449's down restores 189's original text verbatim so a rollback does not leave MUL-6951 wording describing a model the database no longer runs. The same stale semantics remained in prose at six code sites — the plain and manual dispatch entry points, the scheduled-plan path, the create_issue enqueue comment, the autopilot-level republish path, and the webhook trigger create site. All said a run's authority or accountable human follows the last editor, or that automation resolves rule_owner. These sit on the authorization path, so they are corrected rather than left to drift. published_by is now described as what it is: config audit, with no bearing on a run. Also corrects the backfill comment's migration reference: trigger publishers arrived in 189, not 186. 2. The test file this PR added open-coded INSERT ... RETURNING id with matching t.Cleanup(DELETE ...), which CLAUDE.md prohibits. Rewritten onto testutil.Fixture: a small principalFixture embeds it and every row — members, runtimes, private agents, autopilots, triggers, the second workspace — is built and cleaned up by dbfx. The only remaining DELETE is the mid-test member removal that IS the behaviour under test. Re-verified after the rewrite that both regressions still fail against the old code: reverting admission to the autopilot-creator check reddens the fork test, and dropping the workspace join reddens the cross-workspace test. Verification (real PostgreSQL 17, pristine database): full suite shows only the four pre-existing unrelated failures (cmd/multica, internal/cli, internal/daemon, internal/daemon/execenv). Migration 449 up/down round-trips and the down restores 189's comments. go vet ./internal/... and git diff --check clean. Co-authored-by: multica-agent <[email protected]> * docs(autopilot): state the backfill widening plainly, drop raw-SQL test seeding Fourth round of Elon's review on #7918. Both items are leftovers from the previous round; no behaviour change. 1. Migration 449's backfill comment carried two factual errors. It said published_by_* arrived in migration 186; it was 189. The previous round intended to fix this but the string replacement did not match the wrapped line and silently did nothing, so the wrong number shipped. It also claimed the backfill "is no wider than the authority those runs carry today", which is false: before MUL-6951 an automatic run had no originator at all, only narrowly-scoped borrow paths, so promoting a backfilled editor to the run's full originator IS a widening. The product decision to accept it stands; the comment now says so directly as a compatibility tradeoff instead of denying the widening. A migration comment that understates what it grants is worse than no comment. 2. The new test file still reached raw SQL transitively. It called seedAttributionFixture, which hand-writes INSERT and cleanup for its user / workspace / member / runtime / agent — so the file's own claim that "every row goes through testutil.Fixture" was untrue and the repository's fixture rule was still unmet. It now bootstraps the base identity with Fixture.User / Workspace / Member on an unbound fixture and adopts it, which also drops the agent and issue that helper builds and these tests never use. The comment explains why the helper is deliberately not reused. Re-verified after the fixture change that both regressions still fail against the old code: reverting admission to the autopilot-creator check reddens the fork test, and dropping the workspace join reddens the cross-workspace test. Verification (real PostgreSQL 17, pristine database): full suite shows only the four pre-existing unrelated failures (cmd/multica, internal/cli, internal/daemon, internal/daemon/execenv). go vet ./internal/..., gofmt and git diff --check clean. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d7247fa9fc |
MUL-6975: preserve departed member identity in timelines (#7931)
* fix(timeline): preserve departed member identity Co-authored-by: multica-agent <[email protected]> * fix(timeline): preserve identity across live updates Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
835c5437a9 |
MUL-6971 fix(issues): stop rejecting description edits against a stale baseline (#7926)
* fix(issues): stop rejecting description edits against a stale baseline The description editor autosaves on a 1.5s debounce and sent the base it had adopted; the server rejected the write whenever that base was not byte-identical to the stored Markdown. Two ways that fired with no second writer present: - The editor normalizes its base (`normalizeMarkdown` = trim) while the server compares raw bytes, so any description stored with surrounding whitespace — every CLI/integration write that keeps a trailing newline — rejected EVERY edit, permanently. - A save whose own echo arrived while the editor was dirty could not advance the base (ContentEditor refuses external syncs on a dirty document), so the next autosave carried a base the server had already moved past. Entering the conflict state then fed the local draft back in as the base, wedging the editor for the rest of the session. The guard also never covered the writers most likely to race a human here: mobile omits text baselines deliberately, and the CLI/agent path sends no base at all. It mostly rejected the user's own autosave. Descriptions are now last-write-wins. `description_base` stays in the request because it is also the channel-media merge metadata — dropping it would make media the editor had adopted impossible to delete. Title and comment baselines are untouched: both save explicitly, send an un-normalized base, and are not reachable by either failure above. Co-authored-by: multica-agent <[email protected]> * test(issues): build the new description-baseline test on the shared fixtures The test landed on the legacy pattern this package is trying to stop growing: `insertWorkflowTestIssue` plus a hand-written cleanup, raw SQL to seed the column under test, and a recorder/status quartet. Rebuild it on `dbfx.Issue` and `testutil.Call(...).Want(...)`, which own the cleanup and print the request line and response body on a mismatch. Behaviour is unchanged — still red against the pre-fix handler with the reported 409, green after it. The assertions added to the surrounding legacy test stay in their host's style; the rule covers new tests. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9fce92f427 |
MUL-6846: feat(cli): expose active and cross-issue agent runs on issue runs (#7922)
* feat(cli): expose active and cross-issue agent runs on `issue runs` An agent could already read one issue's task runs, but only as the full execution history — every past run, hydrated with attribution and token usage — with no way to ask the one question coordination actually needs: who is working right now. And nothing at all could answer it across a parent's sub-issues, which is the case #7768 describes: several agents claim sibling children in parallel and each starts overlapping work without knowing the others exist. The claim-time `## Active sibling runs` block does not cover this. It is scoped by agent_id — your OWN other in-flight tasks — so a different agent running on a sibling issue is invisible, and it is a snapshot taken before the turn started. Two query params on the existing task-runs endpoint, and the flags that drive them: multica issue runs <id> --active # in-flight runs on this issue multica issue runs <id> --siblings # ...and across the sub-issue family `--siblings` roots at the issue's parent, or at the issue itself when it has none. One rule covers both directions: a child sees its siblings, a parent sees its children, a standalone issue degenerates to its own active runs — so a caller does not have to know an issue's shape before choosing a flag. Rows carry the issue they belong to, ordered running-first and bounded, because a parent that fanned out hundreds of children must not turn an advisory read into an unbounded response. Both reads are advisory. Nothing here reserves an issue or serialises anything; they tell a run whom to coordinate with, and coordination still happens in comments. With neither param the response is byte-identical to before, which the issue-detail execution log and the CLI's short-task-ID resolver both depend on. Co-authored-by: multica-agent <[email protected]> * fix(cli): address review on active/sibling run reads Four fixes from review, all on the same theme: a coordination read has to be cheap and has to be honest about what it did not tell you. - `--active` no longer hydrates usage. ListIssueTaskUsage returns a row per (task, provider, model) for every task the issue ever ran, so the active path was still paying the full-history cost the filter exists to remove — for a column that is near-empty on runs that have not finished. The execution log keeps it. - An invalid `active` value is a 400 instead of a silent fallback. `active=tru` used to answer "who is here now?" with the whole history under a 200, and a pile of finished runs reads as the live ones. Same reason the unknown-scope check already existed, same shape as the comment-list boolean params. - The 50-row family cap now announces itself. The query asks for one row past the cap and sets X-Active-Runs-Truncated when it comes back; the CLI warns on stderr. Without it, "the answer was cut off" and "no run on that sibling" are the same response, and an agent reads the second. The cap is also documented in the flag help, the skill and CLI_AND_DAEMON.md, along with the ordering it depends on. - The skill's new agent-facing contract is pinned in builtin_skills_test.go: that the claim-time block is same-agent only, the `--siblings` command itself, the cap disclosure, and the advisory/no-lock boundary — the last a negative anchor, because an agent that reads these as a lock skips the coordination they exist to prompt. Also reuses service.IssueIdentifier instead of re-deriving the format. Co-authored-by: multica-agent <[email protected]> * fix(server): expose the truncation header to CORS, correct the cap rationale Both from re-review, and both about the cap telling the truth. - HeaderActiveRunsTruncated was missing from corsExposedHeaders. No browser client requests scope=family today, so nothing is broken right now — but a custom response header that is not exposed is unreadable from JS, which is the exact silent-truncation failure the header was added to remove. The registry and its guard test exist because this drifted once already (MUL-5492); completing both is two lines. - The comment justifying 50 was wrong. It claimed real family concurrency is bounded far below the cap so 50 sits above any legitimate answer. It does not: agentconfig.MaxMaxConcurrentTasks lets ONE agent run 50 concurrent tasks, this feature exists for the case where several agents work a family at once, and the returned set includes queued / dispatched / waiting rows that no execution slot bounds at all — a parent that fans out 200 children can have 200 enqueued. So 50 is a response-size budget and nothing more, truncation is an ordinary outcome rather than a pathological one, and that is precisely why it has to be reported. Comment and PR body now say that. Co-authored-by: multica-agent <[email protected]> * refactor(cli): give the family read its own compact row, cap at 20 The family read was reusing AgentTaskResponse — the execution log's row. Measured against real data that is ~1645 bytes per run, of which the caller reads almost nothing: attribution (625 bytes, and its own extra query), result, work_dir, relative_work_dir, trigger_summary, coalesced comment ids. An agent asking "is anyone working next to me?" was spending ~20k tokens of its own context on the answer. ActiveRunSummary carries what the question needs and nothing else: task, issue, agent, status, and timestamps. 367 bytes per row. It is a separate type on purpose — not AgentTaskResponse, and not ActiveSiblingRunData (the claim payload's row, which has no agent field because those runs all belong to the claiming agent by construction). Reusing either would mean an optional field only one caller ever sets, which is the shape this commit removes from AgentTaskResponse. The query now names its columns instead of embedding the task row, so Postgres stops detoasting the JSONB blobs too, and the handler drops attribution hydration along with the field. Cap goes 50 -> 20 (maintainer call): a handful of runs is the normal case, so the budget is set to the normal case. Truncation was already reported and still is, which is what makes a small cap safe. Worst-case response: 80 KB / ~20k tokens -> 7 KB / ~1.8k tokens. The CLI's family table drops COMPLETED and ERROR, which are empty on every row of a read that only returns work still in flight. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
a075e58b80 |
MUL-6961 feat(agent): discover Claude Code's model catalog from the CLI (#7917)
* feat(agent): discover Claude Code's model catalog from the CLI
Claude was the last first-party runtime answering "what models do you
have?" from a hand-maintained list. A static catalog cannot know what the
CLI in front of it can run, so every new Anthropic model opened a window
where Multica offered a model that 400s:
API Error: 400 Claude Code 2.1.246 does not support this model;
version 2.1.251 or newer is required.
Claude Code does have a discovery hook, just not on the command line: the
stream-json control protocol answers a `list_models` control request with
the rows its own /model picker renders. Ask the installed binary and the
window closes at the source — a model it cannot run is either absent or
flagged disabled, computed against the real CLI version and the logged-in
account.
Verified against 2.1.223/228/246/258. The three older builds return Fable
5.1 as a disabled row carrying "Update to 2.1.255+ to use Fable 5.1";
2.1.258 returns it as a normal pick. Those rows are the reason no
per-model minimum-version table is needed here: upstream already computes
one, including the remedy copy, which we forward verbatim and grey out.
No version gate guards the request. An unsupported subtype is answered,
not ignored — every build tested replies "Unsupported control request
subtype" in ~2s and exits 0 — so an old CLI costs one cheap round trip and
falls back. Gating would mean hand-maintaining exactly the kind of number
this change removes.
Notes:
- The static catalog stays, now as the fallback and as the server-side
accept list. It is flagged Catalog.Fallback so a discovery failure is
not pinned in the server's day-long cache (MUL-5549).
- Discovery replaces rather than merges, as with every other dynamic
provider. The picker narrows to the CLI's own list; agents already
pinned to an older model keep working, and manual entry still reaches
them.
- Effort levels now come from each row instead of scraping `claude
--help` and narrowing through the hand-kept claudeModelEffortAllow.
- Catalog ids keep the context-window tag, because `claude-opus-5[1m]` is
what the CLI would really run for that row. That exposed a latent bug
in two capability lookups, which normalized the query side but not the
catalog side; both now normalize both, or a model would fail to find
its own effort picker.
MUL-6961
Co-authored-by: multica-agent <[email protected]>
* fix(agent): keep unrunnable Claude models out of the selectable catalog
Review found the first cut carried "this CLI cannot run it" as a flag on
the model itself. That was wrong in a way a flag cannot fix.
Only the create-agent dropdown learned the flag. The inspector picker and
the agent builder read the same list and kept offering the row, so
editing an existing agent still reached the 400 this change exists to
prevent. And the part no amount of teaching fixes: an already installed
desktop client has never heard of the field, so it renders the row as an
ordinary model and persists a model id the CLI rejects — exactly what the
repo's mixed-version API rule forbids.
Unavailable models now travel in their own list, end to end: daemon
report, store, catalog cache, and the client result. `models` contains
only selectable entries. The inspector and the builder needed no changes
to become correct — they read `models`, and the rows are no longer in it
— and an old client is correct for the same reason. Both pickers render
the advisory rows through one shared component that takes no click
handler, so nothing can turn them back into controls.
Two more review findings:
- The default row was dropped when no sibling row named the same model,
which an org-restricted install can produce. That is not one missing
picker entry: ValidateThinkingLevelWith resolves an empty model — the
common "follow the CLI default" configuration — through the entry
flagged Default, and fails closed without one, silently discarding the
user's effort on every such task. It is now materialised, keeping the
row's own capabilities. The test that claimed to cover this asserted
the opposite of its own comment; rewritten, plus a case proving an
unrunnable default is never materialised.
- "One cheap round trip" for a CLI without list_models was one round trip
PER TASK: every Claude task carrying a thinking_level loads the catalog
before it starts, and cachedDiscovery refuses to memoise a fallback.
The structured unsupported-subtype answer is now remembered per
(command, CLI version) — a property of the binary, not a bad moment —
so an old CLI is probed once. Transient failures stay uncached and
retryable, and the version in the key makes an upgrade take effect at
once rather than after a TTL. Detecting it costs ~0.01s against the
~1.7s it saves, and the fallback path already paid for it.
The discovery timeout keeps its 20s ceiling, now with the measurements
that justify it: 1.57-2.19s over five runs against 2.1.258, 1.64-1.79s
against 2.1.246. The ceiling is for a CLI that never answers, not a slow
one.
Verified against real binaries: 2.1.246 offers four selectable models and
one unavailable row carrying upstream's own upgrade hint, 2.1.258 offers
Fable 5.1 normally, a missing binary falls back to the flagged static
catalog.
MUL-6961
Co-authored-by: multica-agent <[email protected]>
* docs(agent): record the downgrade case as an accepted boundary
Dynamic discovery governs selection, and only selection. An agent already
configured with a model whose CLI is later downgraded still starts and
still gets the upstream 400 — discovery cannot reach a value already
persisted.
That is deliberate, not an oversight, so it is written down next to the
code that invites the wrong fix. Clearing the configured model turns a
legible error into a silent config change the user did not make;
substituting a runnable one is worse, because it succeeds using a model
nobody chose. The downgrade is rare and the upstream error is accurate,
so failing loudly at task start is a fine way to report it.
MUL-6961
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
f01eb5a06c |
MUL-6928 perf(issues): index scalar property contains behind a bigram prefilter (#7878)
* MUL-6928 perf(issues): index scalar property contains behind a bigram prefilter
Scalar `contains` property filtering runs `(properties ->> def) ILIKE
'%needle%'`, which no index can serve, so it degrades to a workspace-wide
sequential scan. At 1M issues that costs ~8x the throughput of an equality
filter, and the pgxpool connections it holds queue every other tenant's cheap
query behind it.
Put a redundant, indexable prefilter in front of the per-key check:
`LOWER(properties::text) LIKE LOWER('%needle%')`, served by a new pg_bigm GIN
index on the same expression. The per-key ILIKE still decides the result; the
prefilter only narrows the candidate set. On a 500k-row pg_trgm proxy a
selective needle goes from a 183ms Parallel Seq Scan to a 1.4ms Bitmap Index
Scan.
The prefilter is skipped, falling back to today's behaviour, for needles it
cannot screen soundly or usefully: jsonb escapes `"`, `\` and control
characters on serialization, so those never appear literally in
properties::text, and a single character carries no bigram to narrow with.
Migration 446 cannot use the DO ... EXCEPTION guard migrations 032 / 036 use
for pg_bigm — a concurrent build cannot run inside one — so the runner gates it
on the operator class instead: databases without pg_bigm record the version
with its SQL skipped and keep the unaccelerated (still correct) filter.
cmd/migrate/README.md documents building it out of band afterwards.
Co-authored-by: multica-agent <[email protected]>
* MUL-6928 fix(issues): prefilter short contains needles too
The single-character skip rule was derived from the pg_trgm proxy used to
develop this change, where a 1-character needle can only drive a sequential or
full-index scan. pg_bigm is different — indexing 1- and 2-character keywords is
the capability it exists for over pg_trgm — so the rule denied the target
extension exactly the searches it was chosen to serve, and a single CJK
character is already a word.
Measured on PostgreSQL 16.15 with pg_bigm 1.2 over 1M issues: a rare
1-character CJK needle goes from a 134ms Parallel Seq Scan to a 1.2ms Bitmap
Index Scan (8.3 -> 10527 tps at 4 clients). Only the JSON-escaping guard
remains, which is about soundness rather than benefit.
Co-authored-by: multica-agent <[email protected]>
* MUL-6928 fix(issues): analyze the properties expression index after building it
Building an expression index does not collect statistics for its expression.
Until they exist the planner falls back to a pattern-length heuristic that
estimates any single-character LIKE at a few percent of the table, so it cannot
tell a needle matching 50 rows from one matching 50,000 — and migration 446's
index ends up built, valid, ready, and never chosen. Autovacuum does not rescue
a large, quiet issue table promptly: its default threshold is 10% of rows
changed.
Measured on PostgreSQL 16.15 with pg_bigm 1.2 over 1M issues, with
max_parallel_workers_per_gather=4: a rare single-character CJK needle plans as a
Parallel Seq Scan at ~101ms with 446 alone — indistinguishable from the
pre-change baseline — and as a 0.64ms Bitmap Index Scan once the statistics
exist. Whether the wrong estimate actually flips the plan depends on how cheap a
parallel scan looks, so without this the shipped behaviour would vary by
instance sizing.
447 is deliberately not gated on pg_bigm the way 446 is: ANALYZE is valid, cheap
(557ms on 1M rows) and side-effect-free everywhere, and refreshing ordinary
column statistics is useful even where the expression index was skipped.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
11bd18a507 |
MUL-6921 fix(daemon): honor the Co-authored-by toggle in existing checkouts (#7868)
* MUL-6921 fix(daemon): honor the Co-authored-by toggle in existing checkouts Turning off the workspace's Co-authored-by switch only affected repos checked out after the flip. The prepare-commit-msg hook the daemon installs appends the trailer unconditionally and lives in the git common directory, so it outlives the checkout that created it: every later commit in an existing checkout kept getting the trailer — including `git commit --no-verify`, which bypasses pre-commit and commit-msg but not prepare-commit-msg. On Gitea the trailer renders as a second commit author with its own avatar, so the stale value is publicly visible. The hook now re-reads the workspace's current setting at commit time from a state file the daemon publishes, instead of freezing the decision into the script at checkout time. The daemon writes that file whenever it resolves the setting: on every settings refresh, on every workspace sync (covers a daemon restart and repo-cache GC, costs no request), and on checkout. A missing state file keeps the trailer, which is what the hook's presence meant before. Settings edits also had no way to reach a running daemon — the server only woke daemons on rename, and the periodic sync deliberately skips the repos/settings endpoint for a tracked workspace. UpdateWorkspace now notifies members' daemons on a settings change too, and that hint refreshes settings for the workspaces the daemon already tracks. Co-authored-by: multica-agent <[email protected]> * MUL-6921 fix(daemon): make the Co-authored-by verdict converge on its own Review found three ways the first cut still left a stale trailer live. A checkout snapshots the setting before its fetch and lock waits, then wrote that snapshot to the workspace-wide state file — so a slow checkout could resurrect a value the user had already turned off. The daemon is now the only publisher: it holds the ordering between settings updates, and the checkout reads what was published (falling back to its snapshot only when nothing has been published yet). Publication itself takes a per-workspace lock and reads the verdict inside it, so a publisher that started before a settings update can no longer overwrite its result. Hooks written by earlier releases read no state at all, so publishing a new value never reached them: an upgraded host kept adding the trailer until each repo was checked out again. Publication now also reconciles the hooks in the workspace's bare caches — enabled rewrites them to the gated script, disabled deletes them, hooks the daemon does not own are left alone — which runs on every settings refresh and every workspace sync. Hook files are published by rename, since these repos have live checkouts committing against them. A settings edit made while the daemon's websocket was down produced a hint nobody received, and the reconnect reconcile only synced workspaces and profiles. It now re-reads settings for tracked workspaces too. Regression tests for all three, each verified failing without its fix; the migration test seeds the previous release's hook verbatim and makes real commits without any CreateWorktree call. Co-authored-by: multica-agent <[email protected]> * MUL-6921 fix(daemon): migrate isolated checkouts' Co-authored-by hooks too The hook sweep only reached bare caches, so a checkout with isolated git metadata — Codex on Linux and the Windows sandbox — kept whatever hook its task workdir was prepared with. On a host that upgraded, that is the previous release's unconditional script, which reads no state: the user turns the setting off, commits in that workdir, and the trailer is still there. That is the reported bug, still live for exactly the checkouts the first sweep could not enumerate. Publication now also walks the daemon's env roots for the workspace and reconciles each checkout that owns its git metadata. Env roots are attributed by their owner record, so one workspace never rewrites another workspace's hooks, and a linked worktree is skipped because its hook belongs to the bare cache. The walk is bounded to the depth repos are checked out at and never descends into a matched checkout. Also makes the publish-ordering regression deterministic: the verdict read is now injected, so the test parks a publisher between taking the lock and reading, and a second publisher reaching the cache during that window is itself the failure. It fails immediately when the read is moved back outside the lock, instead of depending on a 20ms sleep landing right. Co-authored-by: multica-agent <[email protected]> * MUL-6921 fix(daemon): attribute pre-v0.4.35 env roots to their workspace The isolated-checkout sweep required the owner record to name the workspace, which only became true in v0.4.35 ( |
||
|
|
4fe6586b32 |
MUL-6790 feat(issues): support operator matching in scalar property filters (#7765)
* feat(issues): support operator matching in scalar property filters Closes #7692. Scalar custom-property filters (text / number / date / url) previously matched by exact equality only, so a text fragment or a numeric range returned an empty list. A property filter member is now either a plain string (exact equality — the unchanged default that keeps every existing saved view, URL, and API call meaningful) or an operator object: - contains — case-insensitive substring (text / url) - gt / gte / lt / lte — numeric comparison (number) - before / after — lexicographic comparison of the stored "YYYY-MM-DD" strings (date) Backend: parsePropertiesFilterParam accepts {op, value} members and compiles them to operator patterns; propertiesFilterPredicate renders ILIKE / numeric / date-string comparisons and the static ListOpenIssues unroll in issue.sql mirrors them. contains values are LIKE-escaped at parse time and ops are fully validated before compilation, so the SQL branches only ever see legal shapes. The table-filters request carries members as raw JSON so both shapes survive the round-trip, and the query fingerprint canonicalizes them byte-exactly. Frontend: the filter menu grows an operator row above the scalar value input ("is" is the default and still commits a bare string), the client matcher in issueMatchesPropertyFilters mirrors the server predicates, and chips summarize operator members with per-locale phrases or math symbols. Operator members ride in saved-view baselines and chip deltas through a canonical propertyFilterValueKey. Verified: full Go suite (race) and the internal/handler property, facet, and issue-table tests; new unit + DB-backed operator tests on both tiers; pnpm typecheck and the core/views Vitest suites including new operator matrix, menu, and saved-view baseline tests. * test(issues): cover operator members through the table-rows endpoint The properties filter serves through three paths — the paginated list, the open_only static unroll, and the table rows/facets compile — but only the first two had operator-member coverage. The table path has PR-specific code (raw-JSON members in issueTableFiltersRequest and the byte-exact sortedUniqueRawJSON fingerprint) that no test exercised. Drive ListIssueTableRows with operator members ANDed across two definitions so the decode → fingerprint → parsePropertiesFilterParam chain is pinned like the other paths. * fix(issues): canonicalize numeric operator bounds and align empty-needle contains Adversarial review findings: - strconv.ParseFloat accepts forms Postgres ::numeric rejects ("0x1p4" hex-float, "1_000" underscores on older PG). The dynamic predicate binds the parsed float and succeeds while the static open_only unroll casts the raw string inside SQL and 500s — same filter, two list endpoints, different outcomes. Canonicalize the compiled pattern value with strconv.FormatFloat(num, 'f', -1, 64) at validation time; the dynamic re-parse and the static cast now see plain decimals. Regression tests cover the hex-float bound on both serving paths. - issueValueMatchesOperator treated an empty contains needle as match-all ("".includes("") is true) while the server rejects empty operator values — a hand-edited saved-view blob could match every issue client-side. The matcher now refuses empty needles, and the test asserts it against a SET value (the unset case short-circuits before the operator runs and could never catch this). - Server e2e: pin unset-key exclusion for contains / before / after on both serving paths via the seeded empty issue. * fix(issues): align scalar property operator semantics * test(issues): cover boolean contains mismatch * fix(i18n): use exclusive Japanese wording for date before/after filters The `before` / `after` operators compare strictly (`<` / `>` on both the server predicate and the client matcher), so a date equal to the bound is never a match. Japanese 以前 / 以後 mean "on or before" / "on or after" — they include the reference date, so a user filtering 「2026-03-01 以前」 reasonably expects March 1 in the results and does not get it. より前 / より後 carry the exclusive meaning the implementation actually has. English "before"/"after" and Chinese 早于/晚于 are already exclusive and are left unchanged. Korean 이전 / 이후 share the same Sino root and are inclusive by dictionary definition, but they are also the conventional rendering for before/after filters in Korean software — left as-is pending a native speaker's call rather than guessed at. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
108407088c |
MUL-6836: fix(codex): allow explicit standard speed (#7756)
* fix(codex): allow explicit standard speed * fix(codex): negotiate explicit standard support --------- Co-authored-by: L <[email protected]> |
||
|
|
f1ce9d4542 |
MUL-6883 perf(server): stop rescanning settled delegated-failure recoveries (#7820)
* feat(server): retire settled delegated-failure recoveries from the outbox scan ListPendingDelegatedFailureRecoveries had no indexable notion of a finished recovery. Its partial index (343) selected every recovery comment ever written, so each sweeper tick re-proved through four joins and two NOT EXISTS subqueries that history was already handled. LIMIT bounds the rows RETURNED, never the rows CHECKED, so a tick that found nothing still grew more expensive as history accumulated. Add comment.recovery_settled_at and a partial index that holds only unsettled recovery comments, and write the marker inside the transaction that makes a task terminal. Every other exclusion the scan applies is reversible — an issue can leave 'done', a cancelled retry can be superseded — so none of them can be frozen into an index predicate. A delivery receipt on a terminal task cannot be taken back, which is what makes it safe to record as durable state. The marker is deliberately written only once the task is terminal. SetTaskDeliveredCommentIDs replaces rather than appends a receipt while a task is still dispatched, so a reclaim by a differently-capable daemon can drop an id it will not deliver; settling at receipt-write time would freeze that legitimate transient window into a permanently lost recovery. Exhaustion is the one terminal outcome whose receipt lands on a row that may still be running, so it retires the comment directly in the same transaction. An unmarked row is the safe direction: it is scanned exactly as it was before this column existed. That is what makes the rolling-deploy window harmless — an older replica that writes a receipt without the marker only leaves work for the backfill. Co-authored-by: multica-agent <[email protected]> * feat(server): backfill delegated-failure recoveries settled before the marker The marker only covers tasks that reach a terminal status after the writing code ships. History written before that keeps matching the unsettled index predicate and keeps being re-checked on every sweeper tick, which is the cost this change set exists to remove. The walk retires a candidate only when a terminal task already recorded it in delivered_comment_ids — the same monotonic condition the runtime path uses. Its keyset watermark advances over every candidate EXAMINED rather than every candidate marked: a recovery that is genuinely still pending must be left alone, and keying on updated rows would hand back the same watermark forever and never terminate. Separate from migrations and startup, in the shape the other backfill commands already use, so a large comment table cannot block a deploy. Running it is a performance recovery step, not a correctness step — until it finishes the scan behaves exactly as it does today. Co-authored-by: multica-agent <[email protected]> * fix(server): close the review gaps in the settled-recovery backfill and paths Three fixes from review. 1. The backfill reproduced the bug it was written to fix. Its keyset ordered by id while idx_comment_delegated_failure_unsettled is (created_at, id), so the candidate scan read every unsettled row and sorted it to return LIMIT of them; and the receipt test ran a per-candidate EXISTS over agent_task_queue, the largest table in the database. The keyset now matches the index, and the receipt lookup is inverted into one agent_task_queue read per BATCH that hash-joins delivered ids back against the candidates. Measured on 500k comments / 200k tasks / 50k unsettled recoveries: 17,393ms and 816,963 temp blocks before, 235ms and no temp spill after. The candidate scan goes from reading 50,000 rows to reading the 1,000 it needs. Total walk time is flat across batch size (50x228ms, 10x1016ms, 5x2199ms — all ~11s), so batch size only trades how long one statement holds row locks. 1000 stays, now for a measured reason rather than a guessed one. 2. Bulk cancellation paths could strand a delivered receipt permanently unsettled, which lets the new index regrow the history it exists to remove. Manual rerun's pending-slot clear and agent archive were the two named in review; auditing the rest found comment-edit cancellation, both chat-session cancellations, workspace revoke and runtime teardown in the same position. All now settle — inside the caller's transaction where one exists, and through CaptureCancelledTasks for bulk paths that finalize outside one. The invariant is stated on the exported helper so the next such path has somewhere to read it. 3. --max-batches could not actually resume. The watermark lived only in the process, so a bounded rerun restarted at the front of the index — which is precisely where the rows this walk never settles live, so repeated bounded runs would rescan the same prefix and never reach the settleable history behind it. Adds --checkpoint-file (atomic replace, read on start) and explicit --after-created-at/--after-id, and logs the resume cursor on every exit path including SIGTERM. Co-authored-by: multica-agent <[email protected]> * fix(server): make delegated-failure settlement terminal-checked and atomic Two fixes from review. 1. The settlement SQL trusted its caller. "A delivery receipt is only immutable once the task is terminal" is the safety premise of the whole design, but the exported helper enforced nothing: passing a dispatched or running task would settle a receipt a reclaiming daemon can still replace, and the marker is monotonic, so nothing could undo it. The predicate now lives in the SQL, so a non-terminal task updates no rows instead of destroying the obligation. 2. Bulk settlement was post-commit best-effort, and the comment claiming the sweeper would replay a failed one was simply wrong: the covering task is terminal and already holds the receipt, so ListPendingDelegatedFailureRecoveries excludes the comment. It would neither replay nor settle — it would sit in the partial index forever, restoring the unbounded scan this change set removes. Every terminal write now commits with its settlement. CancelTasksForIssue / ForAgent / ByTriggerComment and the rerun pending-slot clear run in a transaction; CancelQueuedChatTasks settles inside the one it already had; the sweeper and daemon-recovery bulk fails move behind TaskService wrappers that own the transaction; agent archive gets CancelTasksForArchivedAgent, which cancels and settles atomically while still skipping the per-task broadcast the archive event makes redundant. BroadcastCancelledTasks, CaptureCancelledTasks and HandleFailedTasks run after their caller committed, so they no longer settle at all — a failure there could be neither rolled back nor repaired. The best-effort helper is deleted rather than left available; every remaining call site passes a qtx. Tests: a dispatched/running task with a receipt is not settled (fails without the SQL predicate), a bulk cancel whose settlement fails rolls the cancel back, and the archive path settles through its atomic entry point. Co-authored-by: multica-agent <[email protected]> * docs(server): correct settlement invariant comment and drop leaked test function The SettleDeliveredDelegatedFailureRecoveries doc still described the pre-fix contract — settlement reachable through CaptureCancelledTasks or a post-commit best-effort call — which is exactly the shape the atomicity fix removed. Restate the rule as it now stands (same qtx as the terminal write, never from a post-commit helper) so the stale text cannot guide a future change back into stranding rows in the partial index. TestBulkCancelRollsBackWhenSettlementFails dropped its trigger but left reject_recovery_settlement() behind in the shared test database; drop the function too. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> Co-authored-by: Eve <[email protected]> |
||
|
|
eddb0dc2ad |
MUL-6884 feat(metrics): attribute agent_runtime lookups by product source (#7822)
* feat(metrics): attribute agent_runtime lookups by product source
MUL-6884 asked which product API is behind the hot GetAgentRuntime query.
It has no single answer: a dozen unrelated paths issue the same primary-key
read, so pg_stat_statements sums them into one row and can report that the
statement is busy without saying which behaviour is driving it. Deciding
between "raise the heartbeat interval" and "back off the 500ms browser polls"
needs that split, and today it is a guess.
Add multica_agent_runtime_lookup_total{source,result} and route every
production read through service.RuntimeLookup, which carries the source with
the connection and classifies pgx.ErrNoRows as not_found rather than as an
error — a deleted runtime is the daemon's self-heal signal, not an outage.
The labels are a closed enum (15 sources x 3 results, all prewarmed at zero)
because this fires on the hottest read in the system; an unclassified call
site degrades to "other" instead of minting a series. No query, no timing
histogram, and no extra SQL on /metrics — this PR only makes the existing
calls countable.
An AST lint test fails CI on any direct Queries.GetAgentRuntime call outside
the wrapper, since a bypassed call site would quietly undercount whichever
source it belonged to and send the follow-up work after the wrong caller.
Co-authored-by: multica-agent <[email protected]>
* refactor(metrics): drop the standalone handler lookup file
The two helpers it held are not an endpoint — no route, no HTTP handler, no
new file under cmd/ — but a new agent_runtime_lookup.go under internal/handler
reads like one, and a file name that has to be opened before it can be ruled
out is a bad file name.
Move both into runtime.go next to requireRuntimeReadAccess, where the rest of
the runtime read helpers already live, and name the metrics test after what it
tests. No behaviour change.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
db86c75e12 |
MUL-6878: avoid per-row issue status resolution (#7814)
* perf(server): avoid per-row issue status resolution Co-authored-by: multica-agent <[email protected]> * fix(server): address issue status query review nits Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
bfe32f5e39 |
fix(runtime): wake daemon for local skill requests (#7713)
Co-authored-by: Janez Stupar <[email protected]> |
||
|
|
8d7e6f6a0a |
perf(server): resolve only the requested skill bundles (#7810)
The daemon resolves one skill bundle per request -- deliberately, so each download gets its own size-scaled deadline and is cached independently (GH #4505). ResolveTaskSkillBundles answered each of those by calling LoadAgentSkillBundles, which reads and hashes EVERY skill the agent has, then picks the single requested bundle out of the result and discards the rest. So the per-request cost scaled with the agent's skill count, and the number of requests did too. A cold dispatch for an agent with N skills read the whole skill set N+1 times and computed N*(N+1) bundle hashes to deliver N bundles. #7689 cut each of those loads from N+1 queries to 2, which fixed the round-trip count but not the multiplier: the bytes read from Postgres and the SHA-256 work stayed quadratic, and because the daemon issues these requests serially, that work lands directly in prepare latency. Load only what was asked for: - ListAgentSkillsByIDs is ListAgentSkills narrowed to a set of ids. The junction predicate is unchanged, so it is still the authorization: an id the agent does not have enabled returns no row, which the caller reports as not-found. It is deliberately NOT workspace-scoped, because ListAgentSkills is not either -- a stricter resolve could 404 a ref that claim had just advertised, breaking prepare instead of protecting it. - Files come from ListSkillFilesBySkillIDs, the batch query #7689 added, for those skills only. - Builtins resolve by name from the embedded FS and touch no query. - Only the requested skills are hashed. For 15 skills / 600KB that takes a cold dispatch from ~9.6MB read and 240 hashes to ~1.2MB and 30, with no schema change, no new index, and no change to the wire contract: stale-hash still returns the current bundle, plugin hash pinning still conflicts, response order still follows the request, and a read failure still fails closed with a 500 rather than a 404 the daemon would treat as permanent. LoadAgentSkills and the new loader share one assembly helper, so claim and resolve cannot drift on skill order, per-skill file order, or the nil file list of a skill with no files. Tests: an agent with three skills resolving one must issue one scoped query for one id and never the full ListAgentSkills; a builtin ref must touch no query at all; request order and ref dedup; and the denials the old full-set lookup used to provide -- unassigned skill, disabled assignment, another workspace's skill, unparseable id, a source with no server-side producer -- plus a scoped-read failure returning 500. Both the scoped load and the enabled predicate are mutation-verified. Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
be93feb063 |
MUL-6792: perf(server): batch skill-file loads in LoadAgentSkills (#7689)
* perf(server): batch skill-file loads in LoadAgentSkills LoadAgentSkills issued one ListSkillFiles query per skill inside a loop, so an agent with N skills cost N+1 round trips on the task-claim hot path (three callers in daemon.go). Each query also pulls full file bodies, so the fixed per-query overhead (pool acquire, RTT, parse) was paid N times. Add ListSkillFilesBySkillIDs (skill_id = ANY(...), ordered by skill_id, path — backed by idx_skill_file_skill) and load all files in one query, grouping by skill_id in a single linear pass. N+1 becomes 2. Output order is preserved by assembling in ListAgentSkills order. Total bytes transferred are unchanged; this removes round trips and pool acquires, not payload. The per-skill ListSkillFiles is retained for the single- skill handler callers that legitimately need it. Co-Authored-By: Claude Fable 5 <[email protected]> * fix(server): fail closed when the agent skill read fails Batching the per-skill ListSkillFiles loop into one query changed what a swallowed read error means. Before, one failed query dropped one skill's supporting files. Now a single failure drops the files of EVERY skill -- and the `files, _ :=` discard let claim and resolve carry on as if that were the agent's real skill set. Nothing downstream can catch it. BuildAgentSkillBundles hashes whatever loaded, so the refs sent at claim and the bundle served at resolve agree with each other; the daemon's validateSkillBundle compares a bundle against a ref derived from that same bundle, accepts it, and writes it to the on-disk cache. The agent then starts on skills whose rules are missing, and the next dispatch reuses the cached truncated copy. The same was true of the ListAgentSkills error above it, which could silently strip every workspace skill from an agent. LoadAgentSkills and LoadAgentSkillBundles now return the error, and the three call sites settle it: - claim (both the slim skill-refs path and the legacy inline-skills path) refuses with 500 via rejectClaimSkillLoad and leaves the task dispatched, so the stale-dispatched reclaim redelivers it -- the same treatment rejectClaimSourceLoad gives a transient source read. - ResolveTaskSkillBundles returns 500, which the daemon's existing resolve retry schedule already treats as transient, instead of serving a bundle built from a failed read. Tests: service-level coverage for both read failures, for LoadAgentSkillBundles failing closed rather than returning built-ins only, and for the batch grouping itself -- the fixture puts the two result sets in deliberately different orders (skills by name, files by skill_id) so a positional zip would hand each skill another skill's files, and asserts the per-skill query is no longer issued. Handler-level coverage asserts the 500s and that a refused claim leaves the task dispatched and unstarted. Co-Authored-By: J <[email protected]> Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Claude Fable 5 <[email protected]> Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
46c9ea38a7 |
feat(issues): extend property filter to text / number / date / url (MUL-6423) (#7240)
* feat(issues): extend property filter to text / number / date / url Widen the custom-property filter (value + "No value" / __none__) from select / multi_select / checkbox / actor to the four scalar types. Backend: - parsePropertiesFilterParam emits a jsonb number containment form for numeric filter values, guarding against NaN / Infinity (not representable as JSON). Date / text / url stay on the string form. - Property table facets gain text / url / date (string) and number branches, replacing the 422 default, with the unset "__none__" bucket. Frontend: - isFilterablePropertyType (core) now admits the scalar types; the filter menu renders a value input plus a mutually-exclusive "No value" option for text / number / date / url (PropertyFilterOptions). The scalar value and "No value" are both disabled when a saved view locks the dimension. - Filter chips show the scalar value directly; the client matcher compares number values numerically, matching the server's jsonb containment. - New store action setPropertyFilterValues replaces a property's full filter value set. Tests cover numeric + NaN/Infinity parse, scalar facet buckets, number client matching (incl. non-canonical forms), isScalarPropertyType, and the store action. Note: a text property value literally stored as "__none__" is reserved — the sentinel means "no value", and the filter input rejects it. * fix(issues): address review on scalar property filter (PR #7240) Respond to Bohan-J's review: - Scalar value input could not be typed into: Base UI's menu popup stops every printable key on the role="menu" element, which bubbled up from the input and swallowed characters. The input's onKeyDown now stops propagation for every key except Escape/Tab (Enter still commits and also stops propagation so it cannot activate the highlighted "No value" item). - Scalar facet branches were unbounded: text/url/date/number grouped by distinct value with no LIMIT, but the menu only reads the "__none__" bucket. They now compute just the "__set__"/"__none__" buckets. - New UI surface lacked tests: added issues-header.property-filter.test.tsx driving the real Base UI menu, covering typing, Enter/blur commit, the value <-> "No value" exclusion, and the blur-vs-click ordering. - Draft staleness: sync the draft to the committed scalar value via an effect (keying the input did not reliably reset it), so an external clear cannot be written back by unchecking "No value". - Make onSetValues required; note that isFilterablePropertyType stays an explicit enumeration so future property types opt into filtering. * fix(issues): compose scalar value and No-value filters OR-style Review round 2 on PR #7240: unchecking "No value" dropped a committed scalar value. The draft round-trip engineered for mutual exclusion was the root cause — checking No-value changed the committed set, the draft effect wiped the input, and unchecking read an empty draft. Scalars now treat "No value" as a plain member of the same OR-set as every other property type: - Checking/unchecking toggles membership only and never touches the committed value member, so no restore-on-uncheck machinery is needed. - Committing a value replaces just the value member and preserves existing No-value membership. - The string branch of issueMatchesPropertyFilters skips the sentinel when matching stored values, so a literal "__none__" text value cannot satisfy a No-value filter (agreeing with the server's key-absence predicate). Component tests cover the exact reviewed sequence (commit -> check -> uncheck restores the value) and the uncommitted-draft path. --------- Co-authored-by: deo <deo@deo的macbook\032pro.tyaio> |
||
|
|
d6ecf4bc8a |
fix(issues): derive a usable status key from non-ASCII display names (MUL-6749) (#7638)
Creating a custom issue status whose display name is written entirely in a
non-Latin script — 客户确认, 고객 확인, 確認待ち — failed with "cannot derive a
status key from that name; provide one explicitly". The key alphabet is ASCII,
so such a name slugified to nothing, and the settings form exposes no field for
an explicit key: there was no way to create the status through the UI at all.
The key stays an ASCII machine handle — it is what issue.status stores, what the
CLI takes as an argument, and what the column CHECK permits — so there is NO
migration. When the name has nothing to slug the key falls back to the status's
CATEGORY plus an ordinal (in_review_2), which keeps saying which platform
behavior the status inherits.
Also fixed in the same path:
- Two names that collapse onto one slug ("待客户 Review" and "待供应商 Review"
both slug to "review") no longer make the second create fail with a conflict
blaming a display name nobody took. Disambiguation counts ARCHIVED rows, since
idx_issue_status_workspace_key is not partial.
- Every create takes the exclusive catalog lock, not just the deriving one, so
an explicit-key insert cannot land between a derive's catalog read and its
insert and hand a 409 to a form with no key field.
- The ordinal scan is bounded by the catalog rather than a constant.
A derived key is opaque, so the places that printed a key alone now carry the
name: issue payloads gain status_name (HTTP and background events alike, from
one catalog read), the unknown-status 400 lists `key (Name)`, creating a status
surfaces the key it minted, and `multica issue status --help` says the argument
is a key rather than a display name.
Fixes #7627. Supersedes #7636.
|
||
|
|
08a1e6e934 |
MUL-6742: fix archived-agent runtime garbage collection (#7641)
* fix(runtime): garbage collect archived-agent runtimes Co-authored-by: multica-agent <[email protected]> * fix(runtime): address GC review feedback Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d13e76a557 |
fix(billing): prompt upgrade before quick create (MUL-6746) (#7625)
* fix(billing): prompt upgrade before quick create * fix(billing): use public core export * fix(billing): keep issue-limit recovery actionable * test(handler): provide retry workspace context * fix(billing): respect issue-limit prompt dismissal * fix(billing): clarify issue-limit recovery paths |
||
|
|
5fa65bd125 |
feat(billing): enforce Cloud issue count limits (#7619)
* feat(billing): enforce Cloud issue count limits * fix(billing): address issue limit review feedback * test(billing): exercise validation as manager * fix(billing): resolve second review feedback * fix(core): preserve entitlement limit discrimination |
||
|
|
09a2410e88 |
refactor(agents): rename starter_prompts to conversation_starters (MUL-6709) (#7592)
* refactor(agents): rename starter_prompts to conversation_starters
The UI, docs and all four locales call this feature "Conversation starters"
(#7573), but every identifier behind it still said `starter_prompts` — the DB
column, the wire field, the `/api/config` capability flag, the TS types, the
i18n keys, and the `?focus=` token that a user can read in their own address
bar. The old name is also the one #7573 rejected on the way in: it promises a
prompt, while selecting a starter only prefills the composer.
Rename it everywhere so the identifier matches the product.
This is a breaking rename with no compatibility shim, and it is free exactly
once: the column arrived in migration 404 (#7261) after the last release tag,
so nothing has shipped. No deployed client reads the old name, no stored data
uses it, and `agent_conversation_starters_supported` has no version skew to
straddle. After this release, the same change would cost a wire alias, a
dual-written capability flag, and older Desktop/Mobile builds silently hiding
the editor.
Migration 404 stays as-shipped; 432 renames the column and its CHECK
constraint, so an already-migrated dev or staging database converges with a
fresh one.
Co-authored-by: multica-agent <[email protected]>
* docs(skills): name the conversation-starters surface for agents
`multica-creating-agents` mentioned `conversation_starters` only as a wire
field in two tables. It never said what the feature is called in the product,
where a human configures it, or what selecting one does — so an agent reading
this skill would tell a user about "conversation starters" as a JSON field and
leave them with nowhere to go.
Add the product name (both languages), the Instructions-tab deep link, and the
behaviour that makes the name accurate: selecting one only fills the composer.
Also record that an agent with none configured still shows three built-in
defaults, so "the Chat shows suggestions" is not evidence this agent has any.
Document one gap the field table implied away: the accepted-body list is not a
CLI surface. `conversation_starters` has no `agent create` / `agent update`
flag — only `agent copy` carries an existing value forward — so an agent
following the old text would build a create command that silently drops it.
Co-authored-by: multica-agent <[email protected]>
* fix(onboarding): keep the released starter_prompt field on the pre-v3 shim
The rename in ec44cc0ef swept up `starter_prompt` (singular) on
`bootstrapOnboardingRuntimeRequest`. That is a different contract from the
per-agent `starter_prompts` this PR renames: it is a field on the endpoint
marked "DEPRECATED, kept for desktop < v3", and unlike the per-agent one it
HAS shipped — it is in v0.4.34, released since
|
||
|
|
3b1018e9c9 |
Revert "MUL-6016: feat(task): retry exact Codex capacity failures (#6747)" (#7586)
This reverts commit
|
||
|
|
cd169947b5 |
MUL-6686 feat(daemon): use readable workdir paths (#7316)
Create new task environments under sanitized, bounded readable segments: <workspace-slug>-<workspace-short>/<issue-key>-<task-short>/workdir. The readable name is the visible part; the substance is moving task identity off the directory name. A human-readable segment is mutable — workspace slugs and issue prefixes change — so workspace/task identity now lives in .task_owner, .gc_meta.json and a stable ID-keyed index under .task_roots that freezes one physical root per task for its lifetime. GC and disk-usage read that authoritative identity instead of inferring a workspace from a path string, which also fixes active and preparation-crashed roots being attributed to a nonexistent workspace. Existing workdirs are not renamed or migrated, legacy roots are adopted into the index, and both layouts render privacy-safe relative paths. Labels are lowercased ASCII with separators and traversal collapsed, segments are capped at 24 characters so the two together stay within the previous 49-character Windows path budget, and stable ID suffixes drawn from the UUIDv7 random tail keep roots distinct across renames and case-insensitive filesystems. Closes #6063 Co-authored-by: iiwish <[email protected]> Co-authored-by: J <[email protected]> |
||
|
|
7b8c783991 | fix(issues): let verified autopilot runs assign private agents (#7572) | ||
|
|
bbba9c5460 |
MUL-6697: enforce runtime access on task claims (#7571)
* fix(tasks): enforce runtime access on claims Co-authored-by: multica-agent <[email protected]> * fix(tasks): settle invalid runtime claims explicitly Co-authored-by: multica-agent <[email protected]> * test(tasks): cover runtime owner claim mismatch Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
54027ba763 |
MUL-6639: fix(skills): metadata-only skill listings and stall-based timeouts (#7574)
* MUL-6639: feat(skills): list skill files as metadata, not content `multica skill get` and `multica skill files list` both downloaded every file body to render output that shows none of them — `skill get`'s table view prints four columns, and the file listing prints paths and timestamps. Past a few hundred KB that stopped being waste and became a hard failure: a 599KB skill could not be fetched at all, and the listing that would have shown which file was oversized required downloading all of them first, so the diagnostic died with the thing it was meant to diagnose (GH #7498). Both endpoints now take `?include=content|metadata`. The metadata shape carries path, byte size and content hash per file, plus the size and hash of the SKILL.md body — computed in Postgres, so the bodies never leave the database. Response size now tracks file count, not content size. `GET /api/skills/{id}/files` defaults to metadata: it is a list endpoint, the CLI is its only caller, and `GET /api/skills` already dropped content for this exact reason (GH #2174). `GET /api/skills/{id}` keeps returning content by default — installed web and desktop builds call it for the skill editor and cannot be retrofitted with a query parameter. The CLI asks for metadata on both and takes `--with-content` when the bodies are actually wanted. The file listing gains a SIZE column, which is what makes an oversized skill diagnosable without downloading it. Co-authored-by: multica-agent <[email protected]> * MUL-6639: fix(cli): fail skill transfers on stall, not elapsed time `http.Client.Timeout` is a wall clock on the whole request, body included, so it punishes exactly the transfer that is working: a 599KB skill arriving steadily over a slow link was cut off at 30s mid-body, while a genuinely dead connection was held open for the same 30s (GH #7498). Raising the constant moves the cliff without removing it — the honest test is not "has this taken too long?" but "is it still making progress?". internal/cli/stall.go adds a client that asks the second question: bounded connect, TLS and response-header phases, and a response body that fails after 15s with no new bytes. The wall clock stays as a loose 10-minute backstop — demoted, not deleted, because dropping it to 0 would have left a stalled request upload with no deadline at all. Failures now say which thing went wrong. A new KindNetworkStalled reports "no data received for 15s after N bytes" rather than a timeout, because only one of those is fixed by waiting longer. Body-read errors are classified at all now: wrapTransport only ever saw errors from http.Client.Do, which returns once the headers arrive, so the failure #7498 actually reports reached users as a raw "context deadline exceeded ... while reading body" out of the JSON decoder. MULTICA_HTTP_STALL_TIMEOUT sets the no-progress budget. An explicitly set MULTICA_HTTP_TIMEOUT still applies on this path, keeping its plain meaning — the longest you will wait for a server — instead of silently reverting to a total-elapsed limit. Adoption starts with the skill commands, where the failure was reported; the mechanism itself is not skill-specific. Graduation is one edit: point NewAPIClient at NewStallAwareHTTPClient, fold StallAwareContext into APIContext, drop newSkillAPIClient. A permanent second client would leave the CLI with two timeout personalities, which is worse than either alone. Co-authored-by: multica-agent <[email protected]> * MUL-6639: fix(skills): hash raw UTF-8, keep the no-include default at content Review found two problems with the previous two commits. **The file hash was not over the file's bytes.** `sha256(content::bytea)` runs the bytea *input* parser over the text rather than taking its bytes, so it reads backslash escapes: a file containing `\x41` hashed as the single byte `A`, disagreeing silently with the Go-side hash of the SKILL.md body. Worse than the mismatch, and not caught earlier because the first check used accented text with no backslash in it: `content::bytea` *errors* on a bare backslash. `C:\Users\...`, a regex `\d+`, a LaTeX snippet — ordinary contents of a skill's supporting files — fail with "invalid input syntax for type bytea", making the listing 500 on the skills most likely to need it. `convert_to(content, 'UTF8')` takes the bytes, and matches Go exactly. **`/api/skills/{id}/files` must not change what a bare request returns.** The previous commit flipped its default to metadata on the grounds that the CLI is its only caller. That was the wrong reading of "only caller": the CLI is installed software too, and an older `skill files list --output json` reads `content`. It is the same argument that kept the detail endpoint's default — applied inconsistently to a client that upgrades separately from the server. Both endpoints now return content when `?include=` is absent. The shrink travels with the caller instead: the CLI sends `include=metadata` itself, so GH #7498 is fixed with no synchronized release, and the default can flip once clients that send `include=content` have aged in. The default is now uniform, so resolveSkillInclude no longer takes a per-endpoint parameter. Tests: TestSkillFileHashCoversRawUTF8Bytes covers `\x41`, invalid hex, bare backslashes, Unicode and empty bodies against the Go hash — verified to fail on the previous SQL. TestSkillEndpointsWithoutIncludeStillReturnContent pins the compatibility guarantee on both endpoints. Not addressed, and noted as follow-up: the metadata path still loads the full SKILL.md into Go, because loadSkillForUser needs the row for its tenant check. The HTTP response no longer grows with content; the database read still does. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
8fda4f22d5 |
MUL-6016: feat(task): retry exact Codex capacity failures (#6747)
* docs: record Codex capacity retry design * docs: add Codex capacity retry implementation plan * fix(taskfailure): recognize exact Codex capacity error * feat(task): retry exact Codex capacity failures * feat(server): configure Codex capacity retries * docs: expose Codex capacity retry setting * fix(task): align capacity retry comments --------- Co-authored-by: cuijs <[email protected]> Co-authored-by: Claude <[email protected]> |
||
|
|
baf1bbf340 |
fix(billing): make Cloud seat enforcement automatic (#7542)
Replace the separate Fleet/capacity URLs and seat-capacity switches with the single managed MULTICA_CLOUD_URL connection. Strict seat admission turns on whenever Multica is Cloud-connected and fails closed when the capacity machine token is absent or invalid; the recovery worker starts only for a valid executor so live intents keep their retry budget during credential outages. Split invitation throttling into a non-consuming precheck and post-reservation consumption. Persistent capacity_full and capacity_overcommitted rejections charge only the actor budget; transient failures charge none. Preserve every Cloud/proxy HTTP 429 as retryable with both Retry-After forms, defer outbox rows without spending attempts, and use Cloud's rate-limit scope so a workspace 429 delays only that tenant while global or unscoped 429s stop the current batch. BREAKING: delete/unset MULTICA_FLEET_URL, MULTICA_CLOUD_FLEET_URL, MULTICA_CLOUD_FLEET_TIMEOUT, MULTICA_SUBSCRIPTION_CAPACITY_ENABLED, MULTICA_SUBSCRIPTION_CAPACITY_URL and MULTICA_SUBSCRIPTION_CAPACITY_WORKER_ENABLED before rollout. The startup guard rejects any non-empty value, so MULTICA_SUBSCRIPTION_CAPACITY_ENABLED=false still blocks boot. Configure MULTICA_CLOUD_URL, MULTICA_CLOUD_TIMEOUT and MULTICA_SUBSCRIPTION_CAPACITY_SERVICE_TOKEN instead; entitlements keep their independent MULTICA_ENTITLEMENT_POLICY_* configuration. Companion: multica-ai/multica-cloud#57 |
||
|
|
8442504201 |
fix(runtimes): hide private runtimes from non-owners (#7435)
* fix(runtimes): enforce private runtime access * fix(runtimes): gate agent model discovery * refactor(runtimes): reuse isRuntimeUsableForUser on runtime detail The detail page re-derived the owner-or-public rule inline instead of calling the shared helper every other surface in this PR uses. The two disagreed on an ownerless runtime: the helper is fail-closed (matching the server's read gate, which reuses canUseRuntimeForAgent), while the inline copy rendered Usage for a public one and could only 404. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: m2keloop <[email protected]> Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
6cbb50378f |
MUL-6680: fix autopilot list subscribers + make CLI triggers discoverable (#7552)
* fix(autopilot): populate subscribers in the list projection (MUL-6680)
GET /api/autopilots passed a nil subscriber slice to avoid an N+1, but
AutopilotResponse.Subscribers has no omitempty and autopilotToResponse
builds the slice with make(), so a nil input serialized as "subscribers":
[] rather than being omitted. Every listed autopilot therefore claimed to
have no subscribers while GET /api/autopilots/{id} reported the real ones
for the same row at the same moment.
A silently wrong value is harder to work around than a missing one: the
key is present and reads as authoritative — the field's own doc comment
promises exactly that — so callers cannot tell "none configured" from
"not fetched". It was reported after costing a real misdiagnosis
("subscribers keep being cleared").
The N+1 premise was wrong: autopilot_subscriber's primary key leads with
autopilot_id, so one ANY($1) batch is index-supported and needs no new
index. The batch query mirrors the single-autopilot query's member join
and ordering exactly, so list and detail cannot drift apart on who counts
as a subscriber.
Reported at https://github.com/multica-ai/multica/issues/7549
Co-authored-by: multica-agent <[email protected]>
* feat(cli): make autopilot triggers discoverable (MUL-6680)
trigger-update, trigger-delete and trigger-rotate-url all take a
<trigger-id>, but no command listed them. The ids were reachable — the
detail payload returns "triggers" as a top-level sibling of "autopilot",
and the prefix resolver already reads it — yet that requires knowing the
envelope shape, so in practice the reported workaround was the web UI.
The prefix resolver's own miss message already told users to "run the
list command", which did not exist.
Add `autopilot trigger-list <autopilot-id>`, reusing the detail payload
and the same webhook-credential redaction `get` applies, so ids, kind,
enabled state and schedule are readable without --output json | jq.
Also add NEXT_RUN to the `autopilot list` table. next_run_at has been in
the list payload all along, but the table dropped it, which made a
correctly scheduled autopilot indistinguishable from one with no trigger
at all — the second misdiagnosis in the same report. Times render
relative ("in 2h", "3d ago", "—") to stay narrow and to make "never
scheduled" a visibly distinct value rather than an empty cell; LAST_RUN
moves to the same format for consistency within that table. The `get`
table keeps absolute timestamps — it is a single-row detail view where
precision matters more than width.
Reported at https://github.com/multica-ai/multica/issues/7549
Co-authored-by: multica-agent <[email protected]>
* docs(cli): correct autopilot trigger documentation (MUL-6680)
The trigger section claimed webhook and api kinds were "not surfaced
here" because "there is no server endpoint that fires them yet". Webhook
triggers have both an endpoint and CLI support (`trigger-add --kind
webhook`, `trigger-rotate-url`); only the api kind is unsurfaced.
Also state that `triggers` is a top-level key alongside `autopilot`
rather than nested inside it — reading `.autopilot` and finding no
triggers is what the report described — and document trigger-list.
Co-authored-by: multica-agent <[email protected]>
* fix(autopilot): fail closed when the subscriber read fails (MUL-6680)
Both subscriber projections swallowed a query error and continued with an
empty slice. Because subscribers has no omitempty and is documented as
authoritative, that degradation serialized as "subscribers": [] on a 200
— recreating the exact defect this branch set out to remove: the caller
cannot distinguish "none configured" from "read failed", and a client
that round-trips the response into a full-replace PATCH would silently
wipe a real subscriber list.
An empty array is a claim, not an absence. Return 500 on both the list
and detail paths so the failure is visible and retryable.
The detail path carried this degradation before this branch; it is fixed
here rather than deferred because the list/detail agreement asserted by
TestListAutopilots_SubscribersMatchDetail would otherwise still break
under a failed read — leaving the field untrustworthy in exactly the
condition the fix is about.
Addresses review feedback on #7552.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
f8ec870f30 |
MUL-6629: feat(agents): add per-agent starter prompts for chat (#7261)
* feat(agents): add per-agent starter prompts Co-authored-by: Agent Op <[email protected]> * fix(agents): default omitted starter prompts Co-authored-by: Agent Op <[email protected]> * fix(migrations): renumber agent starter prompts Co-authored-by: Agent Op <[email protected]> * fix(chat): make starter prompt prefills reliable Co-authored-by: Agent Op <[email protected]> * fix(agents): preserve starter prompt writes Keep dirty starter-prompt edits across agent refetches and fail closed when an older server cannot persist them. Clarify that copied starter prompts have no dedicated CLI override. Co-authored-by: Agent Op <[email protected]> * fix(migrations): avoid starter prompt prefix collision Main now uses migration prefixes through 403, so move the starter-prompts migration to the next available prefix before requesting another review. Co-authored-by: Agent Op <[email protected]> * fix(agents): preserve edits after failed save Ignore optimistic instruction snapshots while a save is pending so a rejected update can roll back the cache without erasing the user's submitted starter prompts. Cover the delayed optimistic-update and rollback sequence for both label and prompt fields. Co-authored-by: Agent Op <[email protected]> --------- Co-authored-by: Agent Op <[email protected]> |
||
|
|
3d47913c85 |
fix(issues): allow verified autopilot child assignment (MUL-6161) (#7275)
* fix(issues): allow verified autopilot child assignment * test(issues): cover autopilot private squad assignment * test(issues): harden autopilot assignment coverage |
||
|
|
9e2d9b6d1c |
MUL-6661 feat(channels): add /new and /clear conversation controls (#7468)
* feat(channels): add new and clear conversation controls * fix(dingtalk): resolve bot identity before dispatch * feat(chat): expose channel-created chats consistently * refactor(channels): snapshot task delivery routes |
||
|
|
60a55ec57b |
feat(billing): purchase a seat from the invite flow (#7538)
* feat(billing): purchase seats from full invite flow * fix(billing): harden full-capacity invite flow * fix(billing): explain overcommit with cloud seat occupancy Cloud refuses a reserve on used + reserved, but the overcommit toast filled members_over_capacity_description with the product member count. The most common trigger is a first ledger snapshot whose members alone still fit inside the purchased seats, so the message read "3 members but only 3 purchased seats" and did not explain the rejection. Add occupancy_over_capacity_description, which names occupied seats, purchased seats, members and pending invitations, and feed it from the Cloud summary. members_over_capacity_description stays as-is: the billing tab banner is genuinely member-count driven. |
||
|
|
7f828b44e7 |
fix(billing): prefill first Checkout email (#7529)
* fix(billing): prefill checkout payer email * fix(billing): validate checkout payer identity |
||
|
|
da7131ab59 |
MUL-6625: enforce prepaid member seat capacity (#7491)
* MUL-6625: enforce prepaid member seat capacity
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): address review findings
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): close retry and worker races
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): bound lock waits during rollback
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): pin the cross-repo confirm settlement budget
Multica Cloud must wait longer than this worker's whole retry budget before it
treats a member row as one its ledger never recorded; adopting a member whose
confirm is still retrying counts one person as two seats, and an over-estimate
is what asks a customer to buy seats they do not owe.
That budget was only implicit here — 10 attempts of exponential backoff plus a
tick and a lock wait each — and Cloud's horizon had been set from a single
retry. Extract the backoff into retryBackoff, expose SettlementBudget, and
assert the worst case (29m15s) stays inside Cloud's 45m horizon, so raising
maxAttempts or the backoff cap fails review here instead of widening the window
in which Cloud cannot quantify a ledger.
Co-authored-by: multica-agent <[email protected]>
* Revert "fix(seat-capacity): pin the cross-repo confirm settlement budget"
This reverts commit
|
||
|
|
8c563b49be |
MUL-6555 feat(issues): preserve source context for sub-issues (#7461)
* feat(issues): preserve source context for sub-issues * fix(issues): address source context review findings * fix(migrations): guard source context rollback preflight |
||
|
|
3c4288ddea |
feat(issues): always allow creating custom statuses (MUL-6643) (#7503)
* feat(issues): always allow creating custom statuses (MUL-6643) Custom issue statuses shipped behind a one-way rollout gate: creating one minted a status key an older pod could not interpret, so creation stayed closed until the whole fleet could read one. The fleet is there and the feature has been dogfooded, so the gate goes away — creation now answers to workspace role (owner/admin) alone. The key keeps being published as `true` on /api/config. Installed desktop clients v0.4.30-v0.4.32 gate their "New status" button on that decision and fail closed on absence, which would hide an affordance the server now accepts. Same compat treatment as settings_resource_labels. Co-authored-by: multica-agent <[email protected]> * fix(issues): keep custom_issue_statuses unpublished for old clients (MUL-6643) Publishing the key as an always-on compat decision had it backwards. v0.4.30 shipped the custom-status UI without the four fixes that landed in v0.4.31: cards render in the wrong board column (MUL-6409), the timeline glyph loses the status identity (MUL-6413), built-in colors are wrong (MUL-6440), and the catalog does not sync over realtime (MUL-6458). Those clients gate their "New status" button on this key and fail closed on absence, so NOT publishing it is what keeps a client that cannot render the result from producing one. Same shape as the deliberately unpublished desktop_hang_stack_capture key. Clients from v0.4.33 read no flag at all, so they get the button on update and the key never has to be published again. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
2aa45853d9 |
MUL-6640: fix stale Autopilot subscribers after member removal (#7501)
* fix(autopilot): prune departed subscribers Co-authored-by: multica-agent <[email protected]> * fix(autopilot): serialize subscriber saves with revocation Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
b2b4699ff6 |
MUL-6632: add status and priority filters to Inbox (#7497)
* feat(inbox): add status and priority filters Co-authored-by: multica-agent <[email protected]> * fix(inbox): degrade filters for legacy backends Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c8154f9d1a | feat(selfhost): add daemon server URL override (#7331) | ||
|
|
2284420966 |
MUL-6622: fix(server): authorize squad activity by task provenance (#7489)
* MUL-6622: fix(server): authorize squad activity by task provenance `multica squad activity` rejected every issue that was not itself assigned to a squad, so a leader woken by an `@squad` mention on an agent-owned issue or running a leader task bound to a child issue could not record its evaluation. Because the injected `no_action` rule makes that call the substitute for a comment, the decision left no trace at all. The claim path already keys the squad briefing and the mandatory-recording instruction off the task row (`is_leader_task` + `squad_id`), not off the issue's assignee. `RecordSquadLeaderEvaluation` now uses the same source: - load the `X-Task-ID` row first and require it to be a leader task with a stamped squad; resolve the squad from `task.squad_id`; - drop the `issue.assignee_type == "squad"` precondition; - authorize against `task.agent_id` rather than the live `squad.leader_id`, and record `activity_log.actor_id` as `task.agent_id`. A mid-run leader change no longer discards a real evaluation, and no longer misattributes it in a way that silently desyncs the `no_action` comment suppression lookup (which matches on `task.agent_id`); - name the task's issue in the cross-issue rejection, since a stage-barrier wake runs on the parent and reaching for the child id is the common slip. Leader agents running a NON-leader task are now rejected: they are not running as the leader, and the runtime only mandates the call for leader tasks. This is the one behavior narrowing, covered by a test. The injected instructions also gain a failure fallback. The comment prohibition is already conditional on the activity existing (comment.go:1851), so a leader whose recording call errored may comment — it just was not told to. It now is, closing the silent-drop path for any remaining failure cause. Fixes #7487 Co-authored-by: multica-agent <[email protected]> * MUL-6622: fix(server): close review findings on squad activity authorization Addresses the four items from PR review. 1. Tenant isolation. `GetAgentTask` is a global lookup by id and the cross-issue rejection quotes the task's issue id, so an unrelated task probed through a readable issue echoed back a foreign workspace's issue id. The task is now loaded with `GetAgentTaskInWorkspace`, and check order is explicit: tenant scope, then "caller owns this task", and only then any message naming a task-derived id. 2. Claim-time role divergence. `is_leader_task` on the row is enqueue-time intent: when the leader is swapped before the claim, the claim path clears `resp.IsLeaderTask` and runs the task as an ordinary agent turn while the row keeps the flag set. Trusting the row alone let such a downgraded run write a leader verdict and suppress its own comment, so the live `squad.leader_id == caller` gate is restored on top of the task ownership gate. `activity_log.actor_id` stays `task.agent_id`, which is what the no_action suppression lookup matches on. Removing the live gate later requires persisting the role the claim actually delivered. 3. Failure fallback vs. one comment per turn. The new "comment if the recording call fails" instruction contradicted the exactly-one-comment rule on the `action` path, where a delegation comment already exists. All four injection sites now scope the fallback to turns that have not commented yet and cap it at one comment. Pinned by TestSquadOperatingProtocolRecordsAgainstTheTurnsIssue and TestBuildPromptSquadLeaderNoActionFailureFallback. 4. Shared test fixtures. The new handler tests are rebuilt on `internal/testutil` (`dbfx.Issue` / `dbfx.Task` / `dbfx.Squad`, `testutil.Call(...).Want(...)`) instead of open-coded `INSERT ... RETURNING` and recorder quartets, per CLAUDE.md. The shared `runningSquadLeaderTaskFixture` moved over too. New negative coverage: foreign-workspace task and foreign-agent rejections both assert the response body does not contain the other issue's id, and a leader change before recording is now asserted to be refused. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |