Files
16f0d307dd fix(realtime): announce comments folded into a queued run (#9702)
A reply posted while the agent already has a queued run in the same thread is
folded into that run on the server: the reply becomes the run's trigger and the
earlier comment joins its covered set. The run's status does not change, so no
task event fired and open issue pages kept the run under the earlier comment,
leaving the new reply with no visible queue row.

Publish task:coalesced after the merge (and after a delegated-failure merge).
Web and desktop already refresh the issue's runs on any task: event, which moves
the queued block under the newest comment it answers; mobile now listens for it
too.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-30 17:45:54 +08:00

227 lines
11 KiB
Go

package protocol
// Event types for WebSocket communication between server, web clients, and daemon.
const (
// Issue events
EventIssueCreated = "issue:created"
EventIssueUpdated = "issue:updated"
EventIssueDeleted = "issue:deleted"
EventIssueMetadataChanged = "issue_metadata:changed"
EventIssueAttachmentsChanged = "issue_attachments:changed"
// Comment events
EventCommentCreated = "comment:created"
EventCommentUpdated = "comment:updated"
EventCommentDeleted = "comment:deleted"
EventCommentResolved = "comment:resolved"
EventCommentUnresolved = "comment:unresolved"
EventReactionAdded = "reaction:added"
EventReactionRemoved = "reaction:removed"
EventIssueReactionAdded = "issue_reaction:added"
EventIssueReactionRemoved = "issue_reaction:removed"
// Agent events
EventAgentStatus = "agent:status"
EventAgentCreated = "agent:created"
EventAgentArchived = "agent:archived"
EventAgentRestored = "agent:restored"
// Task events (server <-> daemon).
// Each event maps to a status transition on agent_task_queue. Front-end
// subscribes by `task:` prefix and invalidates the workspace task
// snapshot, so the granularity here is "what does the user want to see
// change" — not "every internal status flip".
EventTaskQueued = "task:queued" // ∅ → queued (enqueue / retry create)
EventTaskCoalesced = "task:coalesced" // status unchanged (a comment folded into a run not yet claimed, which now answers it)
EventTaskDispatch = "task:dispatch" // queued → dispatched (daemon claim)
EventTaskRunning = "task:running" // dispatched → running (daemon started)
EventTaskWaitingLocalDirectory = "task:waiting_local_directory" // dispatched → waiting_local_directory (daemon parked on a busy local_directory path)
EventTaskProgress = "task:progress"
EventTaskCompleted = "task:completed" // running → completed
EventTaskFailed = "task:failed" // running → failed
EventTaskMessage = "task:message"
EventTaskCancelled = "task:cancelled" // * → cancelled
// Inbox events
EventInboxNew = "inbox:new"
EventInboxRead = "inbox:read"
EventInboxUnread = "inbox:unread"
EventInboxArchived = "inbox:archived"
EventInboxUnarchived = "inbox:unarchived"
EventInboxBatchRead = "inbox:batch-read"
EventInboxBatchArchived = "inbox:batch-archived"
// Workspace events
EventWorkspaceUpdated = "workspace:updated"
EventWorkspaceDeleted = "workspace:deleted"
// Member events
EventMemberAdded = "member:added"
EventMemberUpdated = "member:updated"
EventMemberRemoved = "member:removed"
// Subscriber events
EventSubscriberAdded = "subscriber:added"
EventSubscriberRemoved = "subscriber:removed"
// Activity events
EventActivityCreated = "activity:created"
// Skill events
EventSkillCreated = "skill:created"
EventSkillUpdated = "skill:updated"
EventSkillDeleted = "skill:deleted"
// Chat events
EventChatMessage = "chat:message"
EventChatDone = "chat:done"
// EventChatQuickActions supplements a finished turn with follow-up
// suggestions generated by the daemon's post-completion pass. Sent after
// chat:done; clients that don't know it simply never resolve the
// (equally unknown) pending state.
EventChatQuickActions = "chat:quick_actions"
// EventChatCancelFinalized carries the deferred outcome of a cancelled
// chat task once the daemon has flushed its transcript (or the sweeper
// grace period expired): either a late "Stopped." assistant message or a
// draft restore (#5219). Channel outbounds (Slack/Lark) deliberately do
// not subscribe to it — cancellation stays silent on external channels.
EventChatCancelFinalized = "chat:cancel_finalized"
EventChatSessionCreated = "chat:session_created"
EventChatSessionRead = "chat:session_read"
EventChatSessionDeleted = "chat:session_deleted"
EventChatSessionUpdated = "chat:session_updated"
// Project events
EventProjectCreated = "project:created"
EventProjectUpdated = "project:updated"
EventProjectDeleted = "project:deleted"
EventProjectResourceCreated = "project_resource:created"
EventProjectResourceUpdated = "project_resource:updated"
EventProjectResourceDeleted = "project_resource:deleted"
// Label events
EventLabelCreated = "label:created"
EventLabelUpdated = "label:updated"
EventLabelDeleted = "label:deleted"
EventIssueLabelsChanged = "issue_labels:changed"
// Custom property events. Definitions are archived, never deleted, so
// there is no property:deleted — archive arrives as property:updated.
EventPropertyCreated = "property:created"
EventPropertyUpdated = "property:updated"
EventIssuePropertiesChanged = "issue_properties:changed"
// Issue status catalog events (MUL-6243). ONE event for all four writes
// — create, edit, archive, reorder — rather than a verb per write, because
// the catalog is read as a whole table and every client answers all four
// the same way: re-read it. Splitting it would mint four contracts that no
// consumer can tell apart, and reorder has no single row to name anyway.
// The `action` in the payload is advisory (it makes a frame in devtools
// self-describing); nothing routes on it.
EventIssueStatusChanged = "issue_status:changed"
// Project workflow events (MUL-7420). Same shape as the status catalog
// event: workflows are read as one list, so every write answers the same
// way — re-read it. A project switching workflows publishes
// project:updated instead.
EventIssueWorkflowChanged = "issue_workflow:changed"
// Pin events
EventPinCreated = "pin:created"
EventPinDeleted = "pin:deleted"
EventPinReordered = "pin:reordered"
// Invitation events
EventInvitationCreated = "invitation:created"
EventInvitationAccepted = "invitation:accepted"
EventInvitationDeclined = "invitation:declined"
EventInvitationRevoked = "invitation:revoked"
// Autopilot events
EventAutopilotCreated = "autopilot:created"
EventAutopilotUpdated = "autopilot:updated"
EventAutopilotDeleted = "autopilot:deleted"
EventAutopilotRunStart = "autopilot:run_start"
EventAutopilotRunDone = "autopilot:run_done"
// Squad events
EventSquadCreated = "squad:created"
EventSquadUpdated = "squad:updated"
EventSquadDeleted = "squad:deleted"
// Daemon events
EventDaemonHeartbeat = "daemon:heartbeat"
EventDaemonHeartbeatAck = "daemon:heartbeat_ack"
EventDaemonRegister = "daemon:register"
EventDaemonTaskAvailable = "daemon:task_available"
EventDaemonTaskSupplementAvailable = "daemon:task_supplement_available"
EventDaemonRuntimeProfilesChanged = "daemon:runtime_profiles_changed"
EventDaemonWorkspacesChanged = "daemon:workspaces_changed"
// EventDaemonPendingWork is a runtime-scoped hint that a heartbeat-carried
// request (model discovery, capability discovery, or local-skill import) is
// queued for that runtime. Without it the daemon only learns about the
// request on its next scheduled heartbeat, adding up to one HeartbeatInterval
// (15s by default) of dead wait to an interactive UI flow (MUL-5444). The
// hint carries no work
// itself: the daemon still pulls the request through the normal heartbeat
// claim, so a lost or duplicated hint is harmless.
EventDaemonPendingWork = "daemon:pending_work"
// Generic daemon→server request/response over the WebSocket control
// connection (MUL-4257). The daemon sends EventDaemonRPCRequest with a
// correlation id + method + body; the server replies EventDaemonRPCResponse
// with the same request id. This is the transport for WS-first claim (with
// HTTP fallback) and any future daemon→server RPC.
EventDaemonRPCRequest = "daemon:rpc_request"
EventDaemonRPCResponse = "daemon:rpc_response"
// GitHub integration events
EventGitHubInstallationCreated = "github_installation:created"
EventGitHubInstallationDeleted = "github_installation:deleted"
EventPullRequestLinked = "pull_request:linked"
EventPullRequestUpdated = "pull_request:updated"
EventPullRequestUnlinked = "pull_request:unlinked"
// VCS integration events (Forgejo / Gitea / GitLab)
EventVCSConnectionCreated = "vcs_connection:created"
EventVCSConnectionDeleted = "vcs_connection:deleted"
// Lark integration events. `created` covers both first-install
// (UNIQUE on (workspace_id, agent_id) means at most one row per
// agent) and re-install via UpsertLarkInstallation — front-ends
// treat both as a single "installation appeared / refreshed"
// notification. `revoked` flips status to 'revoked' without
// deleting the row; the audit trail is preserved.
EventLarkInstallationCreated = "lark_installation:created"
EventLarkInstallationRevoked = "lark_installation:revoked"
// Slack installation lifecycle (MUL-3666). Same semantics as the Lark
// events: `created` covers both first install and OAuth re-install (the
// UNIQUE on (workspace_id, agent_id, channel_type) means at most one row
// per agent), `revoked` flips status without deleting the row. Front-ends
// invalidate the Slack installations query on either.
EventSlackInstallationCreated = "slack_installation:created"
EventSlackInstallationRevoked = "slack_installation:revoked"
// DingTalk installation lifecycle follows the same create/revoke semantics
// as Slack's BYO channel installation. BindingUpdated shares the
// dingtalk_installation prefix because it changes the member-scoped account
// identifiers returned by the installation listing.
EventDingTalkInstallationCreated = "dingtalk_installation:created"
EventDingTalkInstallationRevoked = "dingtalk_installation:revoked"
EventDingTalkAccountBindingUpdated = "dingtalk_installation:binding_updated"
// WeCom smart-bot installation lifecycle. Same semantics as Lark /
// Slack: `created` covers both first install and re-install via
// UpsertChannelInstallation (the UNIQUE on (workspace_id, agent_id,
// channel_type) means at most one row per agent); `revoked` flips
// status without deleting the row. Front-ends invalidate the wecom
// installations query on either.
EventWecomInstallationCreated = "wecom_installation:created"
EventWecomInstallationRevoked = "wecom_installation:revoked"
// Telegram installation lifecycle. Same contract as the Slack pair:
// front-ends invalidate the Telegram installations query on either.
EventTelegramInstallationCreated = "telegram_installation:created"
EventTelegramInstallationRevoked = "telegram_installation:revoked"
)