mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-25 12:05:06 +02:00
fix/chat-input-commit-on-handoff
844 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2ed8538018 |
test(chat): guard attachment survival on empty-chat cancel
Cancelling an empty chat task deletes the user message, and attachment.chat_message_id is ON DELETE CASCADE (migration 083), so the detach-before-delete in finalizeCancelledChatMessage is the only thing keeping the user's attachment from being silently destroyed. Nothing covered it. Add a DB regression test that binds an attachment to the cancelled user message and asserts: the row survives the cascade (chat_message_id NULL, chat_session_id retained), the cancel response returns it via cancelled_chat_message.attachments, and a resend re-binds it to the new message. Verified red when the detach step is removed. Related issue: MUL-3364 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f6f90ad87f |
feat(chat): workspace-scoped attachment binding + fire-and-forget send
Uploads are now workspace-scoped: the chat session is created and attachments are bound to the message at send time, so a paste/drop no longer creates an empty session the user never sends. - LinkAttachmentsToChatMessage returns the ids it actually bound; the client diffs requested-vs-bound and warns on partial bind, replacing an extra listChatMessagesPage fetch. - Cancelling an empty chat task detaches attachments before deleting the user message (attachment FK is ON DELETE CASCADE) and returns them via cancelled_chat_message.attachments, so a restored draft can re-bind. - SendChatMessageResponse.attachment_ids has no omitempty: "requested but bound zero" serializes [] so the client can tell it apart from an older server and still warn. - Send is fire-and-forget: it no longer steals focus when the user has navigated to another session (guarded on the live store + new-chat agent id); the reply surfaces via the unread dot. commitInput gets clearEditor so a navigated-away commit doesn't wipe the editor now showing another session, while still clearing the sent draft's data. - Draft restore is session-aware so a failed fire-and-forget send restores into the session it was sent from, never the one the user moved to. - Removed the now-unreferenced migrateInputDraft store action. Verified: core/views typecheck, chat-input (15) / store (3) / api client (24) unit tests, go build + vet, handler SendChatMessage + CancelTaskByUser DB tests. Full make check / E2E left to CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6f2e9aa7a8 |
feat(cli): add --thinking-level to agent create and update (#4170) (#4207)
The agent record already carries a top-level `thinking_level` field —
exposed by `agent get --output json`, settable from the web inspector,
and accepted/validated by `PUT /api/agents/{id}` — but the CLI had no
flag to write it. Scripted or version-controlled agent management could
set `--model` but not the thinking depth, forcing a drop to raw HTTP.
Add `--thinking-level` to `agent create` and `agent update`, mirroring
`--model`: a thin pass-through to the top-level `thinking_level` field.
On update an empty string clears it back to the runtime default (the
server reads it as a tri-state pointer: omitted = no change, "" = clear,
value = set). The CLI deliberately does not enumerate valid levels —
they are runtime/model-specific and the server already owns the catalog
(`agent.IsKnownThinkingValue`, `server/pkg/agent/thinking.go`), returning
a 400 for an unknown value or a runtime with no thinking concept, which
the CLI surfaces verbatim.
- server/cmd/multica/cmd_agent.go: register the flag on both commands,
Changed-gate it into the request body, add it to the no-fields error.
- server/cmd/multica/cmd_agent_test.go: cover create/update send,
unset-omission, empty-clears, the flag-exposed guard, and that a
server-side rejection surfaces to the user.
- multica-creating-agents builtin skill + source map: document the new
CLI write surface and re-derive shifted cmd_agent.go line numbers.
Closes #4170
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Matt Voska <voska@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
eb6dffdbc6 |
MUL-3341: clear incompatible model on runtime switch
Closes MUL-3341 |
||
|
|
6bb8cac9ea |
MUL-3332: daemon picks up new custom runtime profiles without restart (#4225)
* MUL-3332: daemon picks up new custom runtime profiles without restart The workspaceSyncLoop's already-tracked branch refreshed only settings and repos via refreshWorkspaceRepos and never re-fetched runtime profiles, so a custom runtime profile created via the web UI / CLI did not become a registered runtime row until the daemon restarted (or a runtimeGone recovery happened to fire). Detect server-side profile drift each sync tick by hashing the workspace's profile list with profileSetSignature(), caching the digest on workspaceState.profileSetSig, and triggering reregisterWorkspaceAfterRuntimeGone when the live signature differs from the cached one. Steady-state syncs cost exactly one extra GetRuntimeProfiles round trip; only real drift fans out to a Register call. The fetch is best-effort: a 404 / network blip preserves the cached signature so a transient failure cannot loop the daemon into spurious re-registrations. Tests in runtime_profile_drift_test.go cover digest stability under reorder, field-by-field drift detection (add / enable-flip / command_name / protocol_family / fixed_args / visibility), the no-drift hot path (no re-register), the new-profile drift path (single re-register + index update + sig converges), and best-effort fetch error handling. Co-authored-by: multica-agent <github@multica.ai> * MUL-3332: split orphan recovery from profile drift; converge to zero Addresses two blocking review concerns on #4225 (raised by GPT-Boy): 1. Profile drift must not kill running tasks on existing runtimes. The first cut reused reregisterWorkspaceAfterRuntimeGone, which after re-register calls /recover-orphans for every returned runtime ID. The server's RecoverOrphanedTasksForRuntime hard-fails every dispatched/running/waiting_local_directory row on that runtime — the correct response when a runtime row was actually deleted server-side, but a catastrophic false positive on profile drift: a built-in runtime still actively executing the user's tasks would have its work killed just because the user added an unrelated sibling custom profile. Fix: extract applyRegisterResponseInPlace as the shared in-place state converger between the two paths, and stop calling /recover-orphans from the drift path. reregisterWorkspaceAfterRuntimeGone keeps the /recover-orphans call because in that path the rows really were gone. 2. Disabling the only profile on a custom-only daemon must converge. The first cut hit registerRuntimesForWorkspace's len(runtimes)==0 guard and bailed out, so the disabled profile's runtime stayed alive in local tracking and on the server (still polling, still heartbeating, still online for the full 150 s stale-heartbeat window). Fix: introduce ErrNoRuntimesToRegister as a sentinel, have registerRuntimesForWorkspace return profileSig even on the empty case (so the drift path can cache the converged-empty signature), and have the drift refresh's error handler take a convergeWorkspaceRuntimesToZero branch that clears local runtimeIDs / runtimeIndex entries and Deregisters the orphaned IDs so the server marks them offline immediately. The same Deregister step also runs on partial drift (a built-in survives, the disabled profile's runtime drops) so the user sees the dropped runtime go offline within the next sync tick instead of after the 150 s sweep. Tests: - TestRefreshWorkspaceRuntimeProfiles_DriftWithRunningRuntimeSkipsOrphanRecovery (mixed built-in + custom, add another profile, asserts zero /recover-orphans calls). - TestRefreshWorkspaceRuntimeProfiles_DisableConvergesCustomOnlyDaemon (custom-only daemon, disable only profile, asserts local state cleared, signature converges to empty digest, Deregister called with the orphaned ID, no recover-orphans, follow-up tick is no-op). - TestRefreshWorkspaceRuntimeProfiles_DisableOneOfManyDeregistersDroppedID (partial drift: only the dropped ID is Deregistered, surviving built-in is left alone and not orphan-recovered). - TestRefreshWorkspaceRuntimeProfiles_NewProfileTriggersReregister extended to also assert no /recover-orphans calls. - TestRegisterRuntimes_SkipsProfileNotOnPath strengthened to assert the ErrNoRuntimesToRegister sentinel and that profileSig is still returned on the empty path. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
64ce459e30 |
fix(github): preserve early installation webhook metadata (#4193)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1f5cb51d4e |
MUL-3284: Web UI + CLI (custom runtime PR3) (#4177)
* MUL-3284 PR3 (CLI): multica runtime profile subcommands + local path override
- cmd_runtime_profile.go: `multica runtime profile` group — list / create /
update / delete against /api/workspaces/{id}/runtime-profiles, plus set-path
/ unset-path for a per-machine command override. protocol-family validated
client-side via agent.IsSupportedType / agent.SupportedTypes; visibility
validated; update only sends changed flags (protocol_family immutable);
delete surfaces the server 409 body when agents are still bound.
- internal/cli/config.go: ProfileCommandOverrides map[string]string on
CLIConfig (omitempty), through the existing marshal/unmarshal so set/unset
round-trips without dropping other fields.
- internal/daemon: Config.ProfileCommandOverrides, loaded from CLIConfig;
appendProfileRuntimes now prefers an override path when set AND executable,
else falls back to exec.LookPath(command_name), else skips+logs as before.
- Tests: cmd_runtime_profile_test.go (registration, create/update/delete incl.
bad-family + missing-flag + 409 surfacing, set/unset path round-trip,
relative-path rejection, config preservation); cli/config round-trip;
daemon prefers-override / falls-back-when-not-executable.
Verified: go build ./..., go vet, go test ./cmd/multica/... ./internal/daemon/...
./internal/cli/... all pass.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-3284 PR3 (Web): custom runtime profiles in the Runtime page
Single-list integration — no new page, no tabs/grouping. Built-in protocol
families and custom profiles render mixed in one catalog, each row badged
built-in vs custom (progressive disclosure).
- packages/core: RUNTIME_PROFILE_PROTOCOL_FAMILIES (single-source 13-family
whitelist, matches server agent.SupportedTypes + migration 120 CHECK) and
RuntimeProtocolFamily / RuntimeProfile types; api client
list/get/create/update/deleteRuntimeProfile against
/api/workspaces/{id}/runtime-profiles; runtimes/profiles.ts query +
mutation hooks and a 409 "agents still bound" conflict parser.
- packages/views/runtimes: runtime-profile-catalog (mixed built-in+custom
rows), runtime-profiles-dialog (header "+ Add runtime" → step 1 pick
protocol family → step 2 display_name/command_name/description; edit form
for custom; admin-gated), delete-runtime-profile-dialog (confirm + graceful
409), runtimes-page / runtime-list integration.
- i18n: new strings added to all four locales (en, zh-Hans, ja, ko).
- a11y: dialogs are focus-trapped, Esc-closable, labelled; full
create/edit/delete flow is keyboard + screen-reader operable.
Iron rule honored: no generic per-agent args UI here (those stay on Agent
config). fixed_args is not surfaced as a general args field.
Verified: turbo typecheck + lint + test pass for @multica/core, @multica/views,
@multica/web; the @multica/web production build succeeds.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-3284 PR3: hide fixed_args from Web + CLI (not yet wired to launch)
Review fix. fixed_args was surfaced as a working feature, but the daemon does
not splice it into the agent launch command — exposing it promised admins a
no-op. Per the call, remove it from every user-facing surface while keeping the
underlying column/struct "carried but not exposed".
- Web (runtime-profiles-dialog.tsx + runtime-profile-catalog.ts): drop the
detail row, the create body field, the update patch field, and the form
textarea; remove the parseFixedArgs/fixedArgsToText helpers and the
fixedArgs form value. Left a NOTE pointing at the daemon TODO.
- i18n: removed the fixed_args strings from all four locales (en/zh-Hans/ja/ko).
- CLI (cmd_runtime_profile.go): removed the `--fixed-arg` flag from create and
update and stopped sending `fixed_args`; updated the "no fields" message.
Test now asserts the CLI never sends fixed_args.
Untouched (the carried-but-not-exposed layer): the runtime_profile.fixed_args
column, the server handler's accept/return, and the daemon's RuntimeProfile
field — all keep the existing TODO(MUL-3284) to wire it into the launch path
(with a test proving args reach the backend) before any UI/CLI re-exposes it.
Verified: turbo typecheck+lint+test pass for @multica/core and @multica/views;
go build/vet/test pass for ./cmd/multica/.
Co-authored-by: multica-agent <github@multica.ai>
* MUL-3284 PR3: stop exposing profile visibility=private (server forces workspace)
Double-review (Eve) caught a fixed_args-shaped hole: visibility=private was a
user-facing toggle (Web form + detail + CLI), but the three server read paths
(ListRuntimeProfiles, daemon ListEnabledRuntimeProfilesForWorkspace,
DaemonRegister) never enforce it — so a "private" profile's name/command would
leak to other members and could be registered by other machines' daemons
(lateral data leak). Same "don't paint a pie" fix as fixed_args: hide the
control everywhere and force the stored value.
- Server (runtime_profile.go): drop `visibility` from the create + update
request structs; CreateRuntimeProfile always stores 'workspace'
(runtimeProfileDefaultVisibility); UpdateRuntimeProfile no longer accepts it;
removed validRuntimeProfileVisibility. The column + response field stay
(always 'workspace') as the carried-but-not-exposed layer.
- Web (runtime-profiles-dialog.tsx): removed the visibility form fieldset,
the VisibilityOption component, the detail row, the visibility state, and the
create/update submit fields.
- i18n: removed the profile visibility strings from all four locales
(profiles.detail.visibility, profiles.visibility.*, profiles.form.visibility_*).
Top-level runtime/agent visibility strings are untouched.
- CLI (cmd_runtime_profile.go): removed `--visibility` from create/update and
the VISIBILITY list column; removed validateVisibility; stopped sending the
field.
- Tests: new TestCreateRuntimeProfile_ForcesWorkspaceVisibility (POST
visibility:"private" -> response and DB row are 'workspace'); CLI create test
now asserts visibility is never sent.
Follow-up MUL-3308 tracks implementing real creator-visibility (and wiring
fixed_args to the launch path); TODOs left in server/Web/CLI point to it.
Verified: turbo typecheck+lint+test pass (@multica/core, @multica/views);
go build/vet pass; go test ./cmd/multica/... and the full ./internal/handler/
suite pass against a migrated Postgres 17.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
52e76e7b23 |
MUL-3284: server API + daemon (custom runtime PR2) (#4149)
* MUL-3284: add runtime_profile schema (custom runtime PR1) Schema-only foundation for custom runtimes. Additive migration 120: - New workspace-level `runtime_profile` table: the shared, team-visible definition of a custom runtime (e.g. an in-house Codex wrapper). protocol_family is CHECK-constrained to the exact backend list in agent.New() (server/pkg/agent/agent.go). The only args column is `fixed_args` (args every agent on the runtime must inherit); there is deliberately no generic per-agent args field — those stay on agent.custom_args. - `agent_runtime.profile_id` (nullable, FK -> runtime_profile ON DELETE CASCADE): NULL = built-in runtime, non-NULL = a registered instance of a custom profile. - Partial unique index agent_runtime_workspace_daemon_profile_key on (workspace_id, daemon_id, profile_id) WHERE profile_id IS NOT NULL. The legacy UNIQUE (workspace_id, daemon_id, provider) constraint is left INTACT so the existing registration upsert (ON CONFLICT (workspace_id, daemon_id, provider) in runtime.sql) keeps resolving its arbiter and the server stays green. Converting that key to a partial (WHERE profile_id IS NULL) index and making the upsert profile-aware is PR2's registration work, not this migration. Verified up + down against Postgres 17: full `migrate up` applies 120; schema shows the table, column, partial index and intact legacy constraint; functional checks pass (partial index blocks dup (ws,daemon,profile), allows same profile on another daemon; CHECK and display_name uniqueness reject bad input; legacy ON CONFLICT still resolves; profile delete cascades to instances); down/up round-trip is clean. Co-authored-by: multica-agent <github@multica.ai> * MUL-3284: drop DB FKs/cascade from runtime_profile migration (review fix) Per review (house rule: no new database foreign keys / cascades; relational integrity lives in the application layer): - runtime_profile.workspace_id: drop REFERENCES workspace ON DELETE CASCADE -> plain UUID NOT NULL. - runtime_profile.created_by: drop REFERENCES "user" ON DELETE SET NULL -> plain UUID. - agent_runtime.profile_id: drop REFERENCES runtime_profile ON DELETE CASCADE -> plain UUID. CHECK constraints, UNIQUE (workspace_id, display_name), the workspace index, and the partial unique index agent_runtime_workspace_daemon_profile_key are unchanged. The legacy UNIQUE (workspace_id, daemon_id, provider) constraint remains untouched. Behavioral consequence: the database no longer auto-removes a profile's agent_runtime instance rows on profile delete. That cleanup moves into PR2's profile-delete path. Up-migration comments document this; down-migration comment no longer references FKs/cascade. Re-verified on Postgres 17: migrate up applies 120; no FK constraints exist on the new columns; partial index still blocks dup (ws,daemon,profile_id); CHECK and display_name uniqueness still reject bad input; deleting a profile now leaves the runtime row orphaned (proving cascade is gone); down/up round-trip clean with the legacy constraint intact. Co-authored-by: multica-agent <github@multica.ai> * MUL-3284 PR2 (server): runtime_profile CRUD + profile-aware registration Server/DB half of the custom-runtime feature. - Migration 121: convert the legacy UNIQUE (workspace_id, daemon_id, provider) constraint on agent_runtime into a partial unique index scoped to built-in rows (WHERE profile_id IS NULL). With 120's partial index on profile_id this lets one daemon host the built-in provider AND custom profiles of the same protocol family without collision. - Queries: runtime_profile CRUD; ListEnabledRuntimeProfilesForWorkspace (daemon-facing); CountAgentsByProfile + DeleteAgentRuntimesByProfile for the app-layer cascade; profile-aware UpsertAgentRuntimeWithProfile; the built-in UpsertAgentRuntime ON CONFLICT now spells out WHERE profile_id IS NULL so it targets the right partial index. sqlc regenerated. - agent.SupportedTypes / IsSupportedType: single-source protocol_family whitelist, in lockstep with agent.New and the migration 120 CHECK. - Handlers + routes: runtime_profile CRUD (member-read, admin-write) with protocol_family whitelist validation, display_name uniqueness (409), and fixed_args validation (no generic per-agent args — iron rule); a daemon-token endpoint GET /api/daemon/workspaces/{id}/runtime-profiles; DeleteRuntimeProfile does the app-layer cascade (delete instance rows then profile, in one tx) and refuses (409) while active agents are bound. - DaemonRegister accepts an optional per-runtime profile_id: validates the profile belongs to the workspace and is enabled, registers via the profile-aware upsert, and skips legacy hostname merge for custom rows. AgentRuntimeResponse now carries profile_id. Verified on Postgres 17: migrate up through 121; built-in + custom codex coexist on one daemon; both upsert arbiters are idempotent; delete-by-profile cascade removes only the custom instance; migrate down reverses 121 then 120 and replays clean. go build ./... and go vet pass; handler test package compiles. Daemon-side wiring (fetch profiles, PATH-resolve command_name, register with profile_id, exec uses command_name) lands in a follow-up commit on this branch. Co-authored-by: multica-agent <github@multica.ai> * MUL-3284 PR2 (daemon): pull profiles, PATH-resolve, register, exec command Daemon-side half of custom runtime profiles, against the server contract on this branch. - client.go: GetRuntimeProfiles(workspaceID) -> GET /api/daemon/workspaces/{id}/runtime-profiles (mirrors GetWorkspaceRepos); RuntimeProfile / RuntimeProfilesResponse types. - types.go: Runtime gains profile_id (parsed from the register response so runtimeIndex carries it). - daemon.go: * appendProfileRuntimes — called inside registerRuntimesForWorkspace before the empty-runtimes guard. Best-effort fetch (older server 404s are logged and swallowed; never fails registration). Per enabled profile: resolve command_name via PATH (exec.LookPath, behind a `lookPath` test hook), skip+log when absent, best-effort version probe, record the resolved absolute path keyed by profile_id, and append a registration entry {name, type=protocol_family, version, status:online, profile_id}. A custom-only host (no built-in agents) still registers. * profileCommandPaths map (guarded by d.mu) + recordProfileCommandPath / customCommandPathForRuntime helpers. * runTask: looks up the claimed task's RuntimeID -> profile command path and overrides the executable path, synthesizing an AgentEntry so a custom runtime runs even when the host has no built-in agent of the same provider. provider (=protocol_family) is unchanged so agent.New still selects the right backend. - Tests: GetRuntimeProfiles request shape; profile runtime appended + path recorded (custom-only host); profile skipped when command not on PATH; profiles-fetch-404 is best-effort; customCommandPathForRuntime bookkeeping. - agent: lockstep test pinning SupportedTypes to agent.New and the migration 120 protocol_family CHECK. Iron rule honored: profile carries no generic per-agent args. fixed_args are parsed and carried but intentionally NOT wired into the launch command yet (optional/best-effort; explicit TODO(MUL-3284) in appendProfileRuntimes). Verified: go build ./... clean; go vet ./internal/daemon/... clean; go test ./internal/daemon/... pass (existing + 5 new); full go test ./internal/handler/ suite passes against a migrated Postgres 17; agent lockstep test passes. Co-authored-by: multica-agent <github@multica.ai> * MUL-3284 PR2: profile delete runs full archived-agent cascade (fix 500) Review fix. DeleteRuntimeProfile previously guarded only on ACTIVE agents, but agent.runtime_id is ON DELETE RESTRICT — a profile whose runtimes had only ARCHIVED agents passed the guard, then DeleteAgentRuntimesByProfile hit the FK and the handler 500'd. Now it mirrors the mature runtime-delete cascade (DeleteAgentRuntime): in one transaction it enumerates the profile's runtime rows, refuses (409) any with active agents or active squads led by archived agents, then for each runtime pauses autopilots pinned to its archived agents, drops archived squads led by them, and hard-deletes the archived agents before removing the runtime rows and the profile. No code path can now fall through to a raw FK error. - queries: ListAgentRuntimeIDsByProfile (sqlc regen). Reuses the existing per-runtime teardown queries (CountActiveSquadsWithArchivedLeadersByRuntime, ListArchivedAgentIDsByRuntime, PauseAutopilotsByAgentAssignees, DeleteSquadsByArchivedAgentsOnRuntime, DeleteArchivedAgentsByRuntime). - tests: TestDeleteRuntimeProfile_ArchivedAgentCascade (archived-only profile deletes cleanly: 204, runtime + archived agent + profile gone) and TestDeleteRuntimeProfile_ActiveAgentBlocks (active agent → 409, survives). Verified against Postgres 17: both new tests pass; full handler suite, daemon tests, and agent lockstep test pass; go vet clean. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1f8f3e8037 |
Fix Office 365 SMTP auth fallback (#4157)
* Fix Office 365 SMTP auth fallback * Fix SMTP auth fallback tests * fix(smtp): address code review feedback for Office 365 auth fallback - Move defer c.Close() after nil check in sendSMTP to prevent panic when openSMTPClient() fails (c can be nil on dial/setup failure). - Add TLS security guard to loginAuth.Start: refuse credentials on unencrypted remote connections (mirroring smtp.PlainAuth behavior), validate expected host name, and allow localhost bypass. - Add isLocalhost() helper for loopback/private-network checks. - Add comprehensive test coverage: loginAuth.Start security checks (unencrypted remote, TLS, localhost, loopback IPs, wrong host), sendSMTP no-panic on dial failure, and full sendSMTP flow tests with mock SMTP server (PLAIN success, LOGIN fallback reconnect, unauthenticated relay). |
||
|
|
18a58e80c0 |
MUL-3316: fix(execenv): switch agent prompt to --content-file to prevent heredoc flag swallowing (#4182) (#4191)
* fix(execenv): switch agent prompt to --content-file to prevent heredoc flag swallowing (#4182) The Linux/macOS reply template recommended --content-stdin with a quoted HEREDOC. That pattern is safe for the trivial single-flag comment-add case that BuildCommentReplyInstructions emits, but as soon as a model wraps extra flags around the heredoc on multica issue create / update — assignee, project — the bash heredoc/flag boundary is fragile in two ways the model cannot see: - A 'BODY \\' terminator with a trailing token is not recognised as the heredoc end, so flag lines after it are swallowed into the description (OXY-78: residual flag text leaked into the description, command exit 0). - A clean terminator turns the trailing '--assignee ...' line into a separate failing shell statement, while the create itself already exited 0 with no assignee (OXY-76: assignee silently dropped, no residual text). In both cases the CLI never receives the swallowed flags, the API request omits the fields, and the daemon has no visibility. The created issue lands with assignee_id: null / project_id: null. This commit: * Switches the Linux/macOS branch of BuildCommentReplyInstructions to --content-file with a 3-step recipe (write file, post, rm) so the body never reaches the shell and all flags live on one shell-token line. There is no heredoc boundary for flags to leak across. * Adds a parallel cleanup step (Remove-Item) to the Windows branch so the cross-platform template is one shape. * Rewrites the runtime_config.go ## Comment Formatting non-Windows section to mandate --content-file and explicitly ban --content-stdin HEREDOC for agent-authored comments, citing #4182. * Reorders the Available Commands menu lines for issue create / update / comment add to put --content-file / --description-file ahead of the stdin variant and add a per-line note pointing at #4182. * Updates and renames the affected tests (TestBuildCommentReplyInstructionsCodexLinux, TestBuildCommentReplyInstructionsNonCodexLinux, TestInjectRuntimeConfigLinuxCommentFormattingEmphasizesFile, TestInjectRuntimeConfigIssueMetadataCodexFormattingUnchanged) so the new file-first contract is pinned and the old HEREDOC mandate is in the banned-strings lists. This converges Linux/macOS with the long-standing Windows file-only path, so the cross-platform guidance is now one shape. It also strictly improves on the previous MUL-2904 guardrail by eliminating shell exposure of the body entirely (no body ever reaches the shell, so backtick / $() / $VAR substitution cannot corrupt it). Closes GitHub multica-ai/multica#4182. No CLI or backend changes — --content-file / --description-file already exist. Co-authored-by: multica-agent <github@multica.ai> * docs(prompt): correct stale BuildPrompt comment to file-first (#4182) --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: CC-Girl <cc-girl@multica.ai> |
||
|
|
2c0f6edca8 |
MUL-3320: feat(lark): add proxy support for WebSocket connections (#4165)
* feat(lark): add proxy support for WebSocket connections - Add Proxy field to GorillaDialer (func(*http.Request) (*url.URL, error)) - Default to http.ProxyFromEnvironment when Proxy is nil, so standard HTTPS_PROXY/HTTP_PROXY/NO_PROXY env vars are respected automatically - Allow explicit override via GorillaDialer.Proxy for custom proxy auth or fixed proxy URLs - Add unit tests for proxy defaults and error forwarding Closes #4032 Co-authored-by: multica-agent <github@multica.ai> * fix(lark): add missing net/url import in ws_connector_test.go TestGorillaDialerProxyDefaults and TestGorillaDialerProxyForwardsError use *url.URL in their Proxy func signatures but net/url was not imported. Co-authored-by: multica-agent <github@multica.ai> * fix(lark): preserve configured websocket proxy Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: J <j@multica.ai> |
||
|
|
4f1797598e |
MUL-3321: Add runtime delete CLI command
Adds a command-line runtime delete flow with strict default behavior and explicit cascade support.\n\nFixes #3909. |
||
|
|
79394ee057 |
MUL-3310: disable bare issue key expansion in comments (#4190)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
241a3582cf |
fix: validate issue status and priority (#4156)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2f24057bc2 |
feat(issues): add date filter (#4129)
Co-authored-by: “646826” <“646826@gmail.com”> |
||
|
|
1afa493165 |
fix(comments): align trigger preview context (#4147)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f9c193e06b |
fix: fail closed on agent task auth tokens (#4142)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ea4f816ce2 |
fix(comments): support edit trigger suppression (#4136)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
63cf0ed308 |
feat(lists): rebuild all six list surfaces on a shared Linear-style list grid (#4038)
* fix(issues): render thread replies in chronological order (#3691)
collectThreadReplies walked the parent_id tree depth-first, so an agent
reply forced to nest under its trigger comment rendered before earlier
sibling replies (A-D-B-C instead of A-B-C-D) whenever the agent returned
late. Sort the collected subtree by created_at (id tie-break) so the
thread reads in arrival order — the same order the server already feeds
agents via `comment list --thread` (ListThreadCommentsForIssue).
All other consumers of the array (resolution derivation, fold bars,
counts, deep-link) are order-independent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): rebuild skills list on shared Linear-style list grid
- new ListGrid primitives (subgrid: single source of truth for column tracks)
- skills list: sortable columns, used-by avatar stack, source/creator columns,
row kebab + batch toolbar with add-to-agent and delete
- skill view store in core; addAgentSkills client method; HoverCheck extracted
to views/common (issues header now imports the shared copy)
- locale keys for list actions/filters and the reworked detail page
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): rework detail page into overview/files tabs
- tabs directly under the breadcrumb header: overview (default) and files
- overview: identity block + rendered SKILL.md as the main column, right
rail with metadata card (source/creator/updated, inline name+description
edit toggle) and used-by panel with bind/unbind
- files: file tree + viewer/editor unchanged; SKILL.md "edit" jumps here
- header kebab menu (copy skill ID, delete); page-level save bar shared by
both tabs; tab state persisted in ?tab=
- file tree: ARIA tree roles + roving-tabindex keyboard navigation
- drop the old right sidebar (metadata dl, permissions paragraph)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* revert(skills): restore detail page to main, keep branch list-only
Drop the overview/files tabs rework from this branch so the PR scope is
the list rebuild only. skill-detail-page.tsx and file-tree.tsx are back
to the main versions; the locale detail/file_tree sections are restored
to match. The detail rework is preserved on stash/skills-detail-tabs
for a follow-up PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): drop description column from skills list
Description is agent-facing routing metadata, not a scannable list
property — Linear's display options expose no description column for
the same reason. Removes the cell, column key, display toggle, lg grid
track, skeleton cells, and the now-dead table.description /
table.no_description locale keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): drive list column hiding by container width, drop by priority
Replace viewport sm:/lg: breakpoints with Tailwind v4 container query
variants (@2xl/@4xl) on the list wrapper, so an open sidebar or split
pane narrows the column set instead of squashing tracks. Remove the
min-w-fit + overflow-x-auto horizontal-scroll fallback: when space runs
out, low-priority columns (created/source/creator, then updated) drop
and return as the container widens; name and usedBy never drop. ListGrid
conventions comment updated — this is the template for all list pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): virtualize list rows with @tanstack/react-virtual
Linear-style headless virtualization: the virtualizer computes the
visible index range and offsets; offsets land as padding on the
scrolling ListGridBody so mounted rows stay direct subgrid children and
column alignment is untouched. Fixed 48px rows skip per-row measurement.
Hideable column tracks move from max-content to deterministic widths
(CSS vars) — with only the visible slice mounted, content-driven tracks
would resize during scroll. A user-hidden column zeroes its var so the
track still collapses; per-cell max-w caps move into the tracks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(skills): list tiers must fit their container trigger width
The @4xl tier's track sum (~1080px with gaps) exceeded its 896px
trigger; with the horizontal-scroll fallback gone, the right-side
columns were clipped unreachably between 896-1080px. Move tier 3 to
@5xl (1024px), trim usedBy/source/creator tracks, and document the
fit invariant with its arithmetic next to the template and in the
ListGrid conventions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): show description as subtext under the skill name
Lives in the name track as a second truncated line (max-w 36rem,
title attr for the full text) — no track, no header, no slot in the
responsive arithmetic. Both lines fit the fixed 48px row, so the
virtualizer contract is untouched; rows without a description center
the name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Revert "feat(skills): show description as subtext under the skill name"
This reverts commit
|
||
|
|
6c17771cce |
fix: re-sign inline attachment media for token-mode clients (#4085)
The two prior MUL-3254 fixes preserved draft/description state across a modal close, but Desktop still could not RENDER the reopened image: in CloudFront signed-URL mode every URL the renderer holds after reopen is unloadable. The persisted record strips the expired signed download_url, the raw CDN url is unsigned (403 on a signed distribution), and the durable /api/attachments/<id>/download endpoint needs credentials that a cross-site file:// <img> fetch cannot carry (web works via the same-site session cookie, which is why the bug was desktop-only). Two changes close the last mile: - /api/config now reports cdn_signed when CloudFront signing is enabled, and pickInlineMediaURL stops picking the raw (unsigned) CDN url in that mode — it is a guaranteed 403. - The Attachment renderer upgrades an auth-gated media URL to a freshly signed one via authenticated GET /api/attachments/<id> (the same re-sign the click-time download path already does), but only on clients without a same-origin /api proxy (api.getBaseUrl() non-empty: Desktop, mobile webview). Cached via TanStack Query with a 20-minute staleTime, inside the server's 30-minute signed-URL TTL. Old servers omit cdn_signed; the schema defaults it to false so behavior is unchanged there. Non-CloudFront deployments return the API path again from the metadata fetch and the renderer keeps the original URL. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
34d4cd3a28 |
feat(openclaw): support connecting to existing OpenClaw gateway (#3260) [MUL-3158] (#3664)
* feat(openclaw): support connecting to existing OpenClaw gateway (#3260) When the daemon host is a lightweight dev machine or CI coordinator, the heavy agent work (LLM inference, code execution, tool use) often belongs on a more powerful remote server already running an OpenClaw gateway. Multica historically hard-coded `openclaw agent --local`, forcing every turn to execute in-process on the daemon host. This change adds an opt-in gateway routing mode controlled per-agent via `runtime_config`: { "mode": "gateway", "gateway": { "host": "...", "port": 18789, "token": "...", "tls": false } } - Backend: ExecOptions gains OpenclawMode + OpenclawGateway; buildOpenclawArgs drops `--local` when mode == "gateway". Per-task openclaw-config.json wrapper pins gateway.{host,port,auth.{mode,token},tls} so users do not need to edit the daemon host's `~/.openclaw/openclaw.json` to point at a different endpoint. - Daemon: AgentData carries the raw runtime_config; decoding is fail-soft (malformed JSON falls back to local mode rather than blocking dispatch). - API: gateway.token is masked to "***" on every GET; PATCH replays the sentinel back, and the update handler restores the persisted token so the round-trip never destroys the secret. Defense-in-depth masking on WS broadcasts, plus String/MarshalJSON masking on the in-memory struct to block stray `%+v` / json.Marshal leaks. - UI: openclaw-only "Routing" tab on the agent detail page with mode selector + structured endpoint form. Token uses a "saved — submit a new value to rotate" UX and matching backend preserve hook. Empty `runtime_config` keeps the historical embedded behaviour, so existing agents are unaffected. * fix(openclaw): address #3664 review — drop dead gateway field, gate pin on mode Per Bohan-J's review: - Remove the dead ExecOptions.OpenclawGateway field (+ its String/MarshalJSON and the daemon.go construction block). It carried the plaintext bearer token but was never read — buildOpenclawArgs only consumes OpenclawMode and the live gateway path runs through execenv.OpenclawGatewayPin — so this narrows the secret's footprint. - Gate the gateway pin on mode=="gateway" in decodeOpenclawRuntimeConfig: a {"mode":"local","gateway":{...,"token"}} payload no longer writes the token into the 0o600 per-task wrapper that --local makes openclaw ignore. - Warn on an unrecognized non-empty mode (e.g. "gatway") instead of silently falling back to local. - Run preserveMaskedGatewayToken in CreateAgent too, so a literal "***" at create time can't persist as a real bearer token. - Document the gateway host:port trust boundary (SSRF note for shared daemon hosts). Adds regression tests for the local-mode pin drop and the unknown-mode warning. |
||
|
|
f415099c4a |
MUL-3263: support managed MCP config for Cursor (#4081)
* feat: support managed MCP config for Cursor Co-authored-by: multica-agent <github@multica.ai> * fix: address Cursor MCP review feedback Co-authored-by: multica-agent <github@multica.ai> * docs: include Cursor in skills MCP support Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7db3e507d1 |
feat(cli): manage workspace repo registry (#4067)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c8ab73d38d |
MUL-3244: Bind quick-create attachments to created issues (#4062)
* fix: bind quick-create attachments to created issues Co-authored-by: multica-agent <github@multica.ai> * test: use real image markdown in quick-create attachment test Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d2a03b8edc |
Fix chat stop and send recovery (#4060)
* Fix chat stop and send recovery Co-authored-by: multica-agent <github@multica.ai> * Fix chat cancel recovery follow-ups Co-authored-by: multica-agent <github@multica.ai> * Guard cancelled chat restore on tx failure Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4594c776e1 |
feat(agent): add CodeBuddy as first-class CLI backend (#3186)
* feat(agent): add codebuddyBackend struct and buildCodebuddyArgs Introduces the codebuddy agent backend skeleton with args builder that mirrors claudeBackend's protocol flags (stream-json, bypass permissions, blocked args filtering) for the codebuddy CLI fork. * feat(agent): implement codebuddyBackend.Execute with stream-json parsing * feat(agent): wire codebuddy into New() factory and launchHeaders * feat(agent): add codebuddy dynamic model discovery from --help * feat(agent): add codebuddy thinking/effort discovery and providerThinkingEnums * feat(daemon): add codebuddy CLI probe, env vars, and args support * fix(agent): use len(models)==0 for default model instead of loop index * fix(agent): increase codebuddy --help timeout to 35s for slow CLI startup * fix(agent): address codebuddy PR review feedback - Wire codebuddy into execenv: reuse claude's CLAUDE.md, .claude/skills, and ~/.claude/skills paths since CodeBuddy is a Claude Code fork - Replace hardcoded 20-min timeout with runContext for zero-timeout = no-deadline semantics matching all other backends - Restore runContext regression tests lost in rebase merge - Mirror claude.go execution model: concurrent stdin write to prevent pipe deadlock, sync.Once for stdin closure, keep stdin open for control_request auto-approval mid-run - Add control_request handling with auto-approve behavior - Add RequestID/Request fields to codebuddySDKMessage - Add codebuddy to metrics knownRuntimeProviders - Add codebuddy to provider-logo.tsx (reuses ClaudeLogo) - Consolidate --help discovery: shared codebuddyHelpOutput cache eliminates duplicate cold-start invocations --------- Co-authored-by: krislliu <krislliu@tencent.com> |
||
|
|
9439a85aa6 |
MUL-3242: fix daemon workdir provisioning race
Fixes GitHub issue #3999 by moving the daemon StartTask transition behind workdir provisioning and extending the active env-root guard through completion metadata writes. |
||
|
|
8151f60c6c |
fix(daemon): drop stale resume session when workdir is not reused (#4027)
CLI backends key their session stores to the cwd (Claude Code looks sessions up under ~/.claude/projects/<encoded-cwd>/), so a prior session id can only resolve when the task runs in the exact workdir the session was recorded against. When the prior workdir no longer exists (GC'd after the issue went done, daemon reinstall, manual cleanup), execenv.Reuse falls back to a fresh Prepare but the stale session id was still passed to the backend: claude exited within a second and the run failed before doing any work — permanently, because the failed run records no session_id and the next claim serves the same stale pointer again. Gate ResumeSessionID on the workdir actually being reused, and correct PriorSessionResumed so the runtime brief uses the cold-path wording when the session is dropped. Fixes multica-ai/multica#3854 (MUL-3221) Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e4ec9dc425 |
MUL-2802: add skill import conflict strategies (#3997)
* feat(skills): structured conflict + overwrite path for local skill re-import
Local-skill re-import previously failed (or silently skipped) on a same-name
collision and, on delete+reimport, changed the skill UUID and dropped agent
bindings. This adds a structured conflict result and a creator-only overwrite
write path so a re-import can update the existing skill in place.
- New terminal import status `conflict` carrying { existing_skill_id,
existing_created_by, can_overwrite }; can_overwrite = requester is the
skill creator (canOverwriteSkillByLocalImport — intentionally narrower than
canManageSkill: admins edit in-app, not via re-import).
- Conflict is detected at daemon-report time (the effective name is only known
once the bundle arrives) via GetSkillByWorkspaceAndName, with the unique
constraint as a race backstop.
- Import requests carry action=overwrite + target_skill_id, persisted through
both the in-memory and Redis LocalSkillImportStore (the heartbeat → daemon
payload is unchanged; overwrite is resolved server-side).
- overwriteSkillWithFiles updates by target_skill_id in one tx: re-checks
existence (workspace-scoped) and creator permission, then replaces
description/content/config and fully replaces files (pruning files absent
from the new bundle). Preserves id, created_by, created_at, name, and
agent_skill bindings. Publishes skill:updated (not skill:created).
- Boundaries: target deleted or permission lost → failed (no fallback to
create-by-name); any mid-write error rolls back the tx, leaving the original
skill untouched. Retrying a terminal request is a no-op.
Tests cover: creator/non-creator conflict (can_overwrite), overwrite preserves
UUID + agent binding + prunes removed files, non-creator overwrite fails,
deleted target fails without create fallback, retry idempotency, and Redis
round-trip of the new fields.
Backend half of MUL-2701. Contract change: same-name local imports now return
status `conflict` instead of `failed` — the Desktop/core client must be updated
to consume it (sibling task).
MUL-2800
Co-authored-by: multica-agent <github@multica.ai>
* fix(skills): gate structured conflict behind client opt-in; guard overwrite target name
Addresses review feedback on PR #3498 (MUL-2800).
Backward compatibility: a same-name local import now returns the new `conflict`
status only when the initiating client opts in via `supports_conflict` (an
overwrite request implies it). Older clients — already-installed Desktop builds
whose poll loop only understands `failed`/`timeout` — keep the legacy `failed`
+ "a skill with this name already exists" behavior, so upgrading the backend
ahead of the client no longer regresses the import UX. This is the installed-app
API-compat boundary the repo's CLAUDE.md calls out.
Also: the overwrite write path now verifies the incoming effective name matches
the target skill's current name (errSkillOverwriteNameMismatch -> failed),
preventing a stale/wrong target_skill_id from writing one skill's content onto
another. Creator-only + workspace scoping already prevent privilege escalation;
this narrows the API so it can't be misused.
Refactored LocalSkillImportStore.Create to a LocalSkillImportRequestInput params
struct (the signature had grown to 8 positional args; the opt-in flag pushed it
over). supports_conflict is persisted in both the in-memory and Redis stores.
Tests: conflict tests now opt in; added a legacy-client test (no flag ->
failed + legacy message) and an overwrite name-mismatch test.
MUL-2800
Co-authored-by: multica-agent <github@multica.ai>
* feat(skills): resolve local import conflicts in desktop
Co-authored-by: multica-agent <github@multica.ai>
* fix(skills): preserve bulk flow after conflict resolution
Co-authored-by: multica-agent <github@multica.ai>
* feat(cli): add skill import conflict strategies
Co-authored-by: multica-agent <github@multica.ai>
* fix(i18n): sync skill import locale keys
Co-authored-by: multica-agent <github@multica.ai>
* docs: explain skill import conflict handling
Co-authored-by: multica-agent <github@multica.ai>
* docs: refresh skill import source map anchors
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
906f70a3e2 |
Add comment trigger preview suppression (#3792)
* Add comment trigger preview suppression Co-authored-by: multica-agent <github@multica.ai> * Use TanStack Query for trigger preview Co-authored-by: multica-agent <github@multica.ai> * Test note comments skip create triggers Co-authored-by: multica-agent <github@multica.ai> * feat(issues): redesign comment trigger chips as avatar chips Single agent renders as avatar + presence dot + full sentence; several agents collapse to an overlapping stack + active count, mirroring the header working chip. Per-agent skip moves into a click-opened popover (hover layers stay read-only tooltips); suppression reads as brightness, not a ban glyph. Loading and preview errors render nothing. Also: share one tooltip body across chip and popover rows, invalidate cached previews after a comment lands (the enqueued task changes the dedup answer), move the preview query key into issueKeys, and drop the now-unconsumed status field from useCommentTriggerPreview. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(server): drop comment trigger wrappers kept only for tests enqueueMentionedAgentTasks and shouldEnqueueSquadLeaderOnComment had no production callers after the compute/enqueue split — the comment path goes through computeCommentAgentTriggers. Tests now exercise the compute functions directly via package-local helpers, so the legacy adapters cannot drift from the real path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(skills): sync mentioning/squads source maps with shared trigger computation The squads source map still pointed the comment-trigger contract at the pre-refactor call chain (comment.go:940 -> shouldEnqueueSquadLeaderOnComment), and the mentioning skill referenced the deleted wrapper. Re-anchor both to computeCommentAgentTriggers / computeAssignedSquadLeaderCommentTrigger / computeMentionedAgentCommentTriggers with current line numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
abf99eb700 |
fix(attachments): server-driven markdown_url + legacy compat (MUL-3192) (#3991)
Comment / issue / chat images uploaded inside the Desktop app rendered
as the broken-image fallback. The editor was persisting a site-relative
`/api/attachments/<id>/download` URL into markdown — that path only
resolves when the document origin proxies /api to the API host (apps/web
via Next.js rewrite). On Electron's file:// origin it never resolved.
Per GPT-Boy's plan, move the durable-URL choice from the client to the
server so the persisted shape is correct regardless of which client
performed the upload.
Server:
- AttachmentResponse gains a markdown_url field, computed by
buildMarkdownURL from the deployment policy:
• storage URL is already absolute + unsigned (public CDN, S3 public
bucket, LocalStorage with MULTICA_LOCAL_UPLOAD_BASE_URL on https) →
use it verbatim;
• CloudFront-signed mode → never expose the raw S3 URL (private
bucket); return cfg.PublicURL + /api/attachments/<id>/download so
the server can re-sign on every request;
• LocalStorage relative + cfg.PublicURL set → same prefixed API
endpoint;
• cfg.PublicURL unset → fall back to site-relative path so web's
Next.js rewrite still works.
- isDurablePublicURL helper rejects URLs carrying CloudFront / S3
signature query params, so a freshly-signed download_url can never
leak into persistence — the original MUL-3130 bug stays closed.
Frontend:
- Attachment type + AttachmentResponseSchema (and apps/mobile mirror)
carry markdown_url. Schema lenient-defaults to '' so a backend old
enough to predate this field doesn't break clients.
- useFileUpload picks markdownLink with three-layer fallback:
(1) att.markdown_url (modern server),
(2) attachmentDownloadPath(att.id) — legacy site-relative shape,
retained for backends old enough to omit markdown_url,
(3) att.url — no-workspace avatar branch with no attachment-row id.
- attachment.tsx keeps the relative→absolute absolutize pass, but
reframed as the legacy-compat fallback for already-persisted
/api/attachments/<id>/download or /uploads/<key> URLs in old
bodies. New content writes absolute URLs and skips this path.
- ContentEditor still tracks freshly-uploaded records into
AttachmentDownloadProvider so Quick Create's editor can swap the URL
via the resolver during the same session even before the server-side
binding lands.
Tests:
- server/internal/handler/file_test.go: 5 new buildMarkdownURL matrix
tests (public CDN passthrough, CloudFront-signed swap, relative
prefixing, PublicURL unset fallback, trailing-slash strip) + 15
table-driven isDurablePublicURL cases.
- packages/core/hooks/use-file-upload.test.ts: new file, 4 cases
covering modern server / legacy server / no-id avatar / oversize.
- packages/views/editor/attachment.test.tsx + content-editor.test.tsx:
10 cases for the absolutize matrix and in-session attachment merge.
- 6 existing test fixtures updated to include markdown_url.
Verification: 1236 @multica/views tests pass; 514 @multica/core tests
pass (4 new); server handler package tests pass for the new matrix
plus all pre-existing TestAttachmentToResponse* and TestDownload*
cases. Typecheck green for views/core/web/desktop. Lint clean on
touched files.
Quick Create attachment_ids binding (orphaned attachment relationship
on the resulting issue) is a follow-up — it requires a new --attachment-id
CLI flag and daemon prompt-template work and is intentionally scoped
out of this PR.
Refs: MUL-3192
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
34c68e1e4c |
fix(comments): enforce single resolution per thread (#3984)
A thread could hold multiple resolved comments at once: ResolveComment was a plain per-row setter that never cleared the prior resolution, and "replacing" one was a display-only illusion (deriveThreadResolution picks the max resolved_at). The stale rows stayed resolved in the DB and the optimistic update flashed the new resolution, then reverted. Make single-resolution-per-thread a write invariant: - ClearOtherThreadResolutions: thread-scoped clear via a RECURSIVE CTE (root + descendants of the target, id <> target), returns each cleared row. - ResolveComment handler runs the clear + set in one tx so the replace is atomic. It emits comment:unresolved per cleared sibling (granular realtime consumers patch a single comment in place and would otherwise keep showing the stale resolution). Target keeps its COALESCE idempotency and the re-resolve event suppression. - Frontend optimistic update mirrors the invariant: resolving clears every other resolution in the same thread, so the cache never shows two at once. Unresolve still only clears its own row. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b1c8eb5f11 |
feat: support Claude Fable 5 pricing (#3982)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ac75c97797 |
fix(desktop): disable auto-start/stop toggles for a daemon the app can't control (WSL2) (#3940)
* feat(daemon): report OS in /health response The desktop app reads daemon liveness over HTTP but starts/stops it via the native CLI, which acts on the host process namespace. On Windows with the daemon in WSL2, /health is reachable via localhost forwarding yet the daemon's process is unreachable — so the app needs a signal to tell a daemon it manages from one it merely sees. Expose runtime.GOOS as `os` so the desktop can compare it against its own host OS. MUL-3154, #3916 Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): disable auto-start/stop for an unmanageable daemon When the daemon runs in an environment the app can't drive — e.g. Linux in WSL2 behind a Windows desktop, reachable only via localhost forwarding — the Auto-start/Auto-stop toggles silently did nothing: the lifecycle CLI acts on the host process namespace and never reaches the daemon's PID. Detect it by comparing the daemon's reported OS (new /health `os` field) against the host OS, and only when a daemon is actually running. When they differ: disable both toggles with an explanatory note, skip the version-match restart on auto-start, and skip the no-op stop on quit. Fails safe — a missing `os` (older daemon) or a matching OS keeps the toggles live, so native Mac/Windows/Linux daemons are unaffected. MUL-3154, #3916 Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): centralize externally-managed guard at the lifecycle boundary Review follow-up. The first cut only disabled the Settings toggles, but the same unmanageable daemon (WSL2 etc.) could still be Stop/Restart-ed from the Runtime card and from automatic lifecycle entries (logout, user switch, reauth, first-workspace restart) — each of which would shell out to a native CLI that can't reach the daemon's process. Move the guard into the main-process lifecycle functions so every entry point is covered by construction: stopDaemon() and restartDaemon() no-op for an externally-managed daemon, and ensureRunningDaemonVersionMatches() treats it as up-to-date (no misleading restart). The per-branch checks in the auto-start handler and before-quit are removed — the boundary now covers them. The Runtime card hides Stop/Restart and shows a 'Managed outside the app' hint, mirroring the Settings tab. Adds a component test for the card's two states. MUL-3154, #3916 Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): preflight the lifecycle guard against live /health Review follow-up. The guard read a cached lastExternallyManaged, which only fetchHealth() updates — but not every lifecycle entry polls before calling stop/restart. syncToken()'s user-switch branch calls restartDaemon() directly after its own fetchHealthAtPort(), without refreshing the cache; on a fresh launch / account switch (no poll yet) the cache is still the initial false, so restartDaemon() would shell out to the native CLI and hit the very WSL/native PID-namespace problem this PR avoids. Make stopDaemon()/restartDaemon() preflight against a live /health read each call instead of trusting the poll cache. The decision is extracted to a pure daemonLifecycleUnreachable(readDaemonOS, hostOS) so a unit test can prove the *live* value (not a cache) drives it. lastExternallyManaged is removed — the UI already reads the per-status externallyManaged field, so it had no other consumer. MUL-3154, #3916 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
72179d1145 |
refactor(transcript): reuse payload helper + cover coalesce timestamps (MUL-3174) (#3958)
* refactor(transcript): reuse taskMessageToPayload in WS broadcast The ReportTaskMessages WebSocket broadcast hand-built the payload and duplicated the created_at formatting that taskMessageToPayload already does. Reuse the helper with the just-inserted row, which carries the same redacted values and the DB-assigned timestamp. Co-authored-by: multica-agent <github@multica.ai> * test(transcript): cover coalesce created_at behavior Lock in that coalescing streaming fragments carries the latest created_at, and falls back to the previous timestamp when the merged fragment has none. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9f21d0b634 |
feat(transcript): add timestamps to run transcript entries (MUL-3174) (#3951)
Threads the existing task_message.created_at column through the full stack (Go protocol -> REST/WS handlers -> TS types -> transcript dialog) so agent run transcripts show per-entry timestamps, helping users spot stalled runs. Additive, no migration. |
||
|
|
c983905d5c |
feat(issues): per-comment thread resolution with sticky collapse (#3910)
* feat(issues): per-comment thread resolution with sticky collapse
Allow resolving any comment, not just roots. Resolving a root folds the
whole thread into one bar (existing); resolving a reply marks it as the
thread's resolution ("Resolve thread with comment") and folds the other
replies behind a "N comments" bar, with the resolution kept visible and
badged. Which comment is the resolution is a pure frontend derivation
(root wins, else latest resolved reply), so no write-side bookkeeping is
needed and any resolved_at combination renders one resolution.
- backend: drop the "only root comments can be resolved" guard
- views: deriveThreadResolution + reply-resolution rendering, sticky
collapse/fold bars (overflow-clip on the card so sticky resolves to the
timeline scroll parent), scroll the folded thread back into view on
collapse, ListChevronsDownUp icon, locales (en/ja/ko/zh-Hans)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(issues): sticky comment headers for long comments
Pin each comment's header (root + replies) to the timeline's scroll
parent while reading, so a long comment keeps its author + actions
visible instead of scrolling out of reach. Exactly one header is pinned
at a time:
- Reply headers stick within their own CommentRow box (release at the
reply's end).
- The root header is wrapped in a root-section container so its sticky
containing block spans only the header + root body — without it the
containing block is the whole thread and the root header stays stuck
behind every reply. Replies render outside the wrapper, gated on open.
- Skip the root header sticky whenever a resolution collapse bar already
owns the top-0 slot (root resolved+expanded, or reply-resolution
expanded) to avoid two bars stacking at the same offset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
70ccbd9bce |
Revert "MUL-3132: harden /uploads/* (auth, no listing, nosniff, tight CSP) (#…" (#3944)
This reverts commit
|
||
|
|
998ebe97e4 |
fix(autopilot): fail create-issue runs on any terminal task failure (#3943)
Generalize SyncRunFromLinkedIssueTask beyond Codex no-progress: any terminal create-issue task failure with no retry still in flight now fails the linked autopilot run, so it can no longer hang in issue_created (invisible to the failure-rate auto-pause monitor). - fail the linked run for any terminal task failure, gated by the existing HasActiveTaskForIssue wait-for-retry guard - remove the isNoProgressTaskFailure classifier (subsumed; drops duplicated pkg/agent marker literals) - drop the redundant GetIssue/origin lookup; GetAutopilotRunByIssue leads and short-circuits ordinary failures in one query - tests: keep no-progress regression, add agent_error (non-retryable) and retry-pending cases Follow-up to #3927. VEN-661 / VEN-662 / MUL-3164 |
||
|
|
13e9485a3b |
MUL-3130: persist stable /api/attachments/<id>/download URL in comment markdown (#3937)
* MUL-3130: persist a stable attachment download URL in comment markdown Comment image attachments rendered as broken placeholders ~30 minutes after upload because the editor was persisting a short-lived HMAC-signed URL into the comment body. After PR #3903 (MUL-3132) hardened /uploads/* with auth, `attachmentToResponse` started signing `attachment.url` as `/uploads/<key>?exp=<unix>&sig=<HMAC>` for LocalStorage so token-auth clients could keep loading inline images. The signature has a 30-min TTL by design — but `useFileUpload` was returning that signed value as `link` and the editor was writing `` straight into the markdown, so the comment permanently captured a URL that stopped working as soon as the signature expired. The fix is to persist a stable per-attachment URL that the server can re-sign on every request: * `useFileUpload` now returns `link = /api/attachments/<id>/download` (avatar uploads without an id still fall back to `att.url` so the pre-attachment-row code paths keep working). * `DownloadAttachment` self-resolves the workspace from the attachment row instead of reading X-Workspace-Slug / X-Workspace-ID headers, and the route is registered under the auth-only group so a native browser <img>/<video> resource load (which cannot attach those headers) succeeds. Membership is checked inside the handler with a 404 deny shape so the route does not act as an IDOR oracle. * A new `GetAttachmentByIDOnly` SQL query supports the workspace- derivation step. * `AttachmentDownloadProvider` now extracts the attachment id from the stable URL when matching markdown refs to attachment records, with a fallback to the existing url-equality check for legacy comments (and S3/CloudFront markdown that points straight at the CDN). * `contentReferencesAttachment` covers both URL shapes for the composer / standalone-list dedup paths so an attachment uploaded before the fix and one uploaded after both deduplicate cleanly. Tests: - New unit tests for the URL helpers (16 tests, packages/core). - Backend regression test: bare `<img src>`-style request without workspace headers now succeeds for a member (200) and 404s for a non-member, replacing the previous "400 without workspace context" contract. - Existing TestDownload*, TestServeLocalUpload*, TestAttachmentTo Response* and the 1220 frontend views tests all pass. Refs: MUL-3130, GitHub issue #3891 Co-authored-by: multica-agent <github@multica.ai> * MUL-3130: address PR review — split markdown link from upload link, swap render src Two follow-ups from GPT-Boy's review on PR #3937. (1) Don't reroute every upload consumer through the workspace-gated download endpoint. The previous change made `useFileUpload`'s `link` field unconditionally return `/api/attachments/<id>/download` whenever the upload had an id. But `useFileUpload` is also used by avatar / logo pickers (account-tab, workspace-tab, agents/avatar-picker, squads/squad-detail-page) that persist `result.link` directly into `avatar_url`. Avatars are referenced cross-workspace (mention chips, member lists, inbox items), so binding their URL to a workspace-membership-gated endpoint would silently break cross-workspace avatar visibility. The fix splits the URL into two semantically distinct fields: - `link` — same as `att.url` (legacy contract). Avatar / logo callers continue to use this and remain on whatever URL semantics the storage backend dictates. - `markdownLink` — the stable per-attachment URL `/api/attachments/<id>/download`. Only the editor's markdown-persisting flow consumes this. Falls back to `link` for the no-workspace upload branch (where there is no attachment-row id to address). `editor/extensions/file-upload.ts` switches `image.src` and `fileCard.href` to `markdownLink ?? link` so comment markdown gets the stable shape while avatar callers stay on `link` unchanged. (2) Make the render-time img src loadable for token-mode clients. Persisting the stable `/api/attachments/<id>/download` URL fixes the expiry problem but the path itself sits behind `middleware.Auth`, which expects either a `multica_auth` cookie or a Bearer token in `Authorization`. Native `<img>`/`<video>` resource loads from token-mode clients (Electron's default mode, the mobile app, legacy-token web sessions) cannot attach the Authorization header, so the bare URL would 401 immediately rather than 30 minutes later. `Attachment.normalize` now runs the resolved record through a new `pickInlineMediaURL` helper that returns: - `record.download_url` when it's an absolute URL with a recognised CDN signature query (CloudFront-signed `Signature` / `Expires` / `Key-Pair-Id`, or `X-Amz-Signature` for raw S3 presigns) — these load as native resource src in any client. - else `record.url`, which on the LocalStorage backend carries a freshly-minted `/uploads/<key>?exp&sig` query whose signature IS the auth (token-mode-loadable). On non-CF S3 backends this is the raw stored URL — same behaviour as today. - else the original input URL (legacy / unresolved markdown keeps its existing path). This gives the same effect for both `kind: "record"` and `kind: "url"` attachment inputs: once a record is in hand, the rendered media src is whichever URL the current backend exposes a working signature on. Tests: - New `file-upload.test.ts` regression pinning that `markdownLink` is what lands in the markdown body when the upload result returns both a short-lived storage URL and a stable download path. - Updated `attachment.test.tsx` to reflect the new render-time swap (the rendered img src now follows the freshly signed URL, not the raw storage URL) and added a record-mode regression pinning the LocalStorage default — when `download_url` is the bare /api/attachments/<id>/download path, the renderer must fall through to the signed `record.url`. - Updated `chat-input.test.tsx` makeUpload helper for the new `markdownLink` UploadResult field. - 1222 frontend views tests + 507 core tests + typecheck across @multica/{core,ui,views} all pass. Refs: MUL-3130, GitHub issue #3891. Builds on |
||
|
|
ee6200de25 |
fix(autopilot): fail no-progress issue runs (#3927)
Fail create-issue autopilot runs that hang in issue_created after a Codex no-progress / semantic-inactivity task failure, so they surface as failed and count toward the failure-rate auto-pause monitor. - route failed create-issue issue tasks (no direct autopilot_run_id) into linked run sync - fail linked runs only for Codex no-progress / semantic-inactivity failures - wait when an active retry task still exists for the issue - add classifier coverage + a DB-backed listener regression VEN-661 / VEN-662 / MUL-3164 |
||
|
|
8b94764c47 |
feat(daemon): configurable OpenClaw binary path / state dir via CLIConfig.Backends (MUL-3157)
Summary: - Add CLI config schema for OpenClaw backend binary path and state dir overrides. - Apply those overrides during daemon LoadConfig using the existing env-var based probe/spawn path. - Cover backward compatibility, precedence, partial overrides, and fail-soft config loading. Verification: - go test ./internal/cli ./internal/daemon - go vet ./internal/cli ./internal/daemon - GitHub CI passed |
||
|
|
7dc05d28bc |
fix(projects): validate project status/priority — return 400 instead of 500 (#3925) (#3939)
* fix(projects): return 400 (not 500) for invalid project status/priority CreateProject/UpdateProject passed an unvalidated status/priority straight to the INSERT, so an unknown value (e.g. --status active) tripped the table's CHECK constraint and surfaced as a blanket 500 'failed to create project' with no server-side log to diagnose it (#3925). Pre-validate both enums against the column CHECK lists and return a 400 with the allowed values. Back it with isCheckViolation -> 400 for any other constrained column, and log the underlying error on genuine 500s so transient DB failures are diagnosable. MUL-3153 Co-authored-by: multica-agent <github@multica.ai> * fix(cli): validate project --status in create/update project create and project update forwarded --status to the server without checking it, while project status already validated. Share a single validateProjectStatus helper across all three so a typo fails fast with the valid list instead of a server round-trip. MUL-3153 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9ff801f926 |
docs(cli): error-message conventions + sign-in copy (PR3, MUL-3104) (#3900)
* docs(cli): add Error Messages conventions + refine sign-in copy (PR3) Final pass of the CLI error-message work (MUL-3104). - CLI_AND_DAEMON.md: new "Error Messages" section documenting the user-facing contract — friendly single-line messages, server validation passthrough, English default with automatic Chinese on a zh locale, the tiered exit codes (0/1/2/3/4/5), --debug / MULTICA_DEBUG for the full chain, and MULTICA_HTTP_TIMEOUT. - cmd_auth.go: clarify three high-frequency sign-in errors so the message states what failed and the next step — local login-callback server start (hints at port/firewall), access-token creation, and token verification (suggests retrying `multica login` and checking the token is valid/not expired). All keep %w so exit-code tiering and --debug detail are preserved. cmd_id_resolver.go is left as-is — its not-found / ambiguous-prefix messages already point at `list --full-id` and need no change. The user-facing FormatError layer is unchanged, so its existing PR1/PR2 test coverage still applies; no test asserted the old verb strings. Refs MUL-3104. PR3 of 3 (final). Co-authored-by: multica-agent <github@multica.ai> * fix(cli): make login failure guidance visible via typed user-message wrapper Addresses 张大彪's PR3 review: the refined sign-in copy was wrapped with %w, so FormatError returned the centralized *HTTPError/*NetworkError copy and the new guidance only appeared under --debug. - Add cli.UserMessageError + cli.WithUserMessage: a typed wrapper carrying a user-facing message that FormatError surfaces by default, recognized before the network/http branches. Unwrap() is preserved, so ExitCodeFor still classifies by the underlying typed error and --debug still prints the full original chain. - cmd_auth.go: wrap the OAuth access-token-creation and PAT-verification failures with WithUserMessage (OAuth copy no longer mentions a passed token, since that flow has none), and move the token-specific 'valid / not expired' hint to the real Enter your personal access token: verification site (was the generic 'invalid token: %w'). - Focused tests: under a wrapped *HTTPError(401) the default FormatError shows the login hint, ExitCodeFor returns ExitAuth, and --debug retains the raw chain; a wrapped *NetworkError still classifies as ExitNetwork. - CLI_AND_DAEMON.md: narrow 'every error' to command errors returned to the top-level handler, noting commands like setup's fast /health probe bypass it. Refs MUL-3104, PR #3900. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8ff68502fc |
MUL-3132: harden /uploads/* (auth, no listing, nosniff, tight CSP) (#3903)
* MUL-3132: harden /uploads/* (auth, no listing, nosniff, tight CSP) Closes the open hardening items from the SVG XSS disclosure (security-findings-2026-06-02). The primary chain (PR #3023 / #3050) is intact; this PR addresses every remaining recommendation from the disclosure's hardening list except 'serve uploads from a separate origin' (a structural change beyond this fix). Changes: - /uploads/* now requires authentication. The route is wrapped in middleware.Auth so anonymous internet users can no longer fetch workspace attachments by guessing the URL. A new ServeLocalUpload handler then enforces the second layer: - workspaces/{wsID}/* paths require membership in wsID (uses MembershipCache for the hot path); - users/{userID}/* paths allow any authenticated user (avatars are referenced cross-workspace); - any other prefix returns 404, so a future feature cannot drop content under /uploads/<other-prefix>/ and inherit a relaxed policy by accident. Non-members see 404 (not 403) so the route does not act as an IDOR oracle for workspace IDs. - Directory listing on /uploads/* is rejected at the storage layer: empty keys, trailing-slash keys, and any key that resolves to a directory return 404 before http.ServeFile would render an HTML index. UUID filenames were obscurity, but enumerating them shouldn't be free. - Every successful /uploads/* response carries X-Content-Type-Options: nosniff and a tight per-response CSP (default-src 'none'; sandbox; frame-ancestors 'none'), overriding the application-wide CSP. This is belt-and-suspenders if a future regression weakens the Content-Disposition: attachment path. - UploadFile rejects HTML-family uploads at the edge (.html, .htm, .xhtml, .shtml, .xht, .phtml, plus a content-type denylist for text/html and application/xhtml+xml so renamed payloads cannot bypass the extension check). SVG and JS remain allowed because their existing serve-side defenses neutralize them and source-code attachments preview as text/plain via /api/attachments/{id}/content. Tests: - storage: TestLocalStorage_ServeFile_RejectsDirectoryListing, TestLocalStorage_ServeFile_HardeningHeaders. - handler: TestIsUploadDenied (pure), TestUploadFile_RejectsHTMLByExtension, TestUploadFile_RejectsHTMLByContentType, TestUploadFile_AllowsLegitimateImage, and the full ServeLocalUpload matrix (RequiresAuth, MemberCanRead, NonMemberDenied, RejectsDirectoryInPath, UnknownPrefixDenied, UserPrefixAllowsAnyAuthedUser). - Full server test suite passes. Co-authored-by: multica-agent <github@multica.ai> * MUL-3132: HMAC-signed query auth for /uploads/* (token-auth client compat) Addresses J's Request Changes review on PR #3903. Problem: PR #3903 wrapped /uploads/* in middleware.Auth, but native <img>/<video>/<iframe> resource loads cannot attach Authorization headers. Token-auth clients (Desktop default, legacy-token Web sessions, mobile) were breaking on inline attachment rendering even though the API itself authenticated fine. Fix: implement HMAC-signed query parameters for /uploads/*, mirroring S3 + CloudFront presigned URLs. - storage.SignLocalUploadURL(rawURL, key, secret, expiry) appends '?exp=<unix>&sig=<HMAC-SHA256(key|exp)>' query params; signature is bound to one specific key, has a TTL matching CloudFront mode (defaultAttachmentDownloadURLTTL = 30 min), constant-time compared on verify. - storage.VerifyLocalUploadSignature(key, exp, sig, secret, now) rejects expired, tampered, wrong-secret, and key-mismatched signatures. - ServeLocalUpload now has two auth paths: signed-query (no Auth middleware needed; signature itself is the authority) and Bearer/cookie (membership-gated as before). Partial signed-query fails closed. - The route in router.go dispatches between the two: if both exp+sig query params are present, route to inner handler unwrapped; else wrap in middleware.Auth. - attachmentToResponse appends signed query to URL when the storage backend is *LocalStorage. CloudFront-signed download URLs and S3 paths are unchanged. Tests: - storage: TestSignAndVerifyLocalUploadURL_RoundTrip, TestVerifyLocalUploadSignature_RejectsExpired, _RejectsTamperedSig, _BoundToKey, _RejectsWrongSecret, TestSignLocalUploadURL_PreservesExistingQuery, TestLocalUploadSignatureFromQuery_EmptyOnAbsence (7 pure tests). - handler: TestServeLocalUpload_{SignedQueryBypassesAuth, SignedQueryRejectsExpired, SignedQueryRejectsTampered, SignedQueryBoundToOneKey, PartialSignedQueryFailsClosed}, TestAttachmentToResponse_LocalStorageMintsSignedURL. Full server test suite passes. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
24b162cdbc |
feat(daemon): surface the real task initiator to the agent runtime (MUL-2645) (#3899)
* feat(daemon): surface the real task initiator to the agent runtime (MUL-2645)
In a multi-person workspace the agent runtime only ever saw the runtime
OWNER identity: the brief's `## Requesting User` is sourced from
runtime.OwnerID and the task-scoped token is owner-bound, so every
requester (whoever commented, @mentioned, or chatted) appeared to the
agent as the owner. Agents that route by initiator for permission,
privacy, or audit all misjudged.
Resolve the real task initiator at claim time and surface it distinctly
from the owner:
- comment / mention trigger -> triggering comment's author (member or agent)
- chat task -> chat session creator (sessions are creator-only)
- on-assign / autopilot / quick-create -> no attributable initiator (omitted)
Adds initiator_{type,id,name,email} to the claim response, the daemon
Task, and TaskContextForEnv, rendered into the brief as a new
`## Task Initiator` section. The section documents the privacy boundary:
the agent's credentials stay owner-scoped, so this is an attested
identity for the agent's own routing/privacy logic, not act-as. No DB
migration — both paths are derivable from existing rows.
Tests: brief rendering (member/agent/omit/sanitize) + email guard unit
tests, and claim-handler tests for the comment and chat paths.
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): store real sender as task initiator, not chat_session creator (MUL-2645)
Review fix (Niko, PR #3899). v1 resolved the chat task initiator from
chat_session.creator_id at claim time. That is correct for web chat and
Lark p2p (creator == sender), but WRONG for Lark group chats: the group
session creator is deliberately the installer (stable identity across
member churn), not the message sender. So in a Lark group, every member
who triggered the agent showed up in the brief as the installer/owner —
the exact bug this issue is about, still live at that entry point.
Capture the real sender at enqueue time instead of deriving it from the
session creator at claim time:
- migration 117: agent_task_queue.initiator_user_id (FK user, ON DELETE
SET NULL); NULL for non-chat and pre-migration rows.
- EnqueueChatTask now takes an explicit initiatorUserID. Web chat passes
the authenticated request user; the Lark dispatcher threads the inbound
sender (binding.MulticaUserID) through scheduleRun -> flushChatRun. The
debouncer keeps the latest scheduled flush per session, so in a multi-
sender silence window the LATEST sender wins (documented + tested).
- claim handler resolves the initiator from task.initiator_user_id and
drops the creator_id fallback entirely.
The Lark group session creator stays the installer (unchanged) — only the
task initiator is corrected, keeping the two concepts cleanly separate.
Tests: dispatcher group regression (initiator = sender, not installer),
latest-sender-wins, p2p initiator assertion; the chat claim handler test
now sets creator != initiator and asserts the stored sender wins.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
26ca943d45 |
feat(lark): add typing indicator lifecycle for inbound messages (#3860)
When a message is successfully ingested, send a Typing reaction to the user's message. When the agent replies (EventChatDone) or fails (EventTaskFailed), clear the reaction before the reply is visible. - Add AddMessageReaction / DeleteMessageReaction to APIClient - Implement reaction HTTP calls in httpAPIClient - Introduce TypingIndicatorManager for per-session state tracking - Wire into Hub (add on ingest) and Patcher (clear before reply) - Skip typing for messages older than 2 minutes (WS replay guard) Co-authored-by: miaolong001 <miaolong@xd.com> |
||
|
|
5be7d1bc17 |
MUL-3136 fix(openclaw): parse config path from last non-empty line of CLI output
Fix OpenClaw config discovery when `openclaw config file` prints Doctor warning UI before the actual config path. The daemon now uses the last non-empty stdout line as the path while preserving the existing tilde expansion, absolute-path validation, stat checks, and fail-closed behavior. Tests: go test ./internal/daemon/execenv |
||
|
|
b83b41ff44 |
feat(cli): per-status error copy with actionable hints (PR2, MUL-3104) (#3897)
* feat(cli): refine per-status error copy with actionable hints (PR2) Builds on PR1's translation layer. Each HTTP-status message now carries an actionable next step, in both English and Chinese: - 401: run `multica login`; plus a self-hosted / non-OAuth fallback telling the user to ask their administrator for valid credentials - 403: check the workspace / ask an admin to grant access - 404: check the ID or run the matching `list` command - 409: re-fetch the latest state and retry - 422: check values / run with --help - 429: wait and retry; reduce call frequency if it persists - 5xx: retry, contact support, and re-run with --debug for the raw response Also adds ErrorKind.String() (stable snake_case identifiers) and uses it in --debug output instead of the raw int, and clears the pre-existing gofmt dirt Eve flagged in cmd_config.go, cmd_version.go, and help.go. Tests: TestErrorKindString (all kinds + uniqueness + out-of-range fallback) and TestFormatErrorActionableHints (locks the per-status hints in EN and ZH). Refs MUL-3104. PR2 of 3. Co-authored-by: multica-agent <github@multica.ai> * test(cli): cover validation (400/422) actionable hint TestFormatErrorActionableHints omitted KindValidation, so deleting the 400/422 hint would have gone unnoticed. Add 400 and 422 cases (no server message, so the generic validation copy is used) asserting EN contains --help / expected format and ZH contains --help / 格式 / 参数. Refs MUL-3104, PR #3897. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
28de8b8bde |
feat(cli): central error translation layer (PR1, MUL-3104) (#3892)
* feat(cli): add central error translation layer (PR1) Introduce server/internal/cli/errors.go, a single user-facing error translation layer that collapses raw transport errors, HTTP status errors, and internal verb-wrapped chains into clear, localized messages. - ErrorKind classification (network timeout/DNS/refused/TLS/offline, 401/403/404/409/400+422/429/5xx, unknown) - NetworkError wraps transport errors and strips the raw URL from the user-facing message; classifyNetworkError categorizes via errors.As/Is with string fallbacks - HTTPError.Kind() maps status codes onto ErrorKind - FormatError: bilingual output (English default, auto-switch to Chinese on a zh LC_ALL/LC_MESSAGES/LANG locale), validation errors surface the server message; --debug / MULTICA_DEBUG appends the full raw chain - ExitCodeFor: tiered exit codes (network=2, auth=3, 404=4, validation=5, other=1) - client.go: default HTTP timeout 15s -> 30s, overridable via MULTICA_HTTP_TIMEOUT; wrap every transport Do() error as *NetworkError - main.go: route errors through FormatError + ExitCodeFor, add persistent --debug flag Unit tests cover every ErrorKind, classification, language detection, exit codes, server-message extraction, and timeout parsing. Refs MUL-3104. PR1 of 3; PR2/PR3 (status-code copy refinement and per-command customization) follow separately. Co-authored-by: multica-agent <github@multica.ai> * fix(cli): address review — unify command timeouts and classify all helper errors Must-fix 1: command-level contexts no longer truncate MULTICA_HTTP_TIMEOUT. Added cli.APITimeout/AtLeastAPITimeout/APIContext (budget = transport timeout + small grace, honoring MULTICA_HTTP_TIMEOUT) and replaced the hardcoded 15s context.WithTimeout in every API command (14 files, 92 sites) with cli.APIContext. The issue-create/comment path now uses APITimeout() with a 60s floor for attachment uploads. Must-fix 2: all API helpers now return *HTTPError on status >= 400. Added a shared newHTTPError(method, path, resp) and routed GetJSON, GetJSONWithHeaders, PostJSON, PutJSON, PatchJSON, DeleteJSON, DeleteJSONWithBody, UploadFile, UploadFileWithURL, DownloadFile (and HealthCheck) through it, so issue update/status/metadata (PUT), comment list (GetJSONWithHeaders), project/label/ comment delete (DELETE) and agent/workspace/autopilot update (PUT/PATCH) all get HTTPError.Kind() classification, friendly copy, and the tiered exit code instead of the raw string + exit 1. Tests: new errors_integration_test.go drives the real helpers against a fake server and asserts FormatError copy + ExitCodeFor for 401/403/404/422/500 across all 10 helpers, plus a slow-server test proving the command context does not cancel before the transport timeout. Updated the UploadFileWithURL assertion to check for *HTTPError. Refs MUL-3104, PR #3892. Co-authored-by: multica-agent <github@multica.ai> * fix(cli): make remaining fixed-timeout API commands honor MULTICA_HTTP_TIMEOUT Closes out the timeout work: the last API command paths still used a hardcoded context deadline that capped MULTICA_HTTP_TIMEOUT. Converted them to cli.AtLeastAPITimeout(<original floor>) so the env override scales them up while preserving each original lower bound: - cmd_autopilot.go autopilot trigger 30s -> AtLeastAPITimeout(30s) - cmd_attachment.go attachment download 60s -> AtLeastAPITimeout(60s) - cmd_agent.go avatar upload 60s -> AtLeastAPITimeout(60s) - cmd_skill.go skill import / search 60s -> AtLeastAPITimeout(60s) - cmd_runtime.go runtime update 150s -> AtLeastAPITimeout(150s) - cmd_login.go workspace-creation poll 10s -> AtLeastAPITimeout(10s) The login poll keeps a short 10s floor to stay responsive within its 5-minute loop, but it is NOT a silent exception: AtLeastAPITimeout means it still scales with MULTICA_HTTP_TIMEOUT. Documented in code and covered by a new subtest in TestAPITimeoutRespectsEnv. Refs MUL-3104, PR #3892. Co-authored-by: multica-agent <github@multica.ai> * style(cli): gofmt cmd_attachment.go to unblock backend CI Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |