Files
multica/server/pkg/protocol/events.go
Naiyuan Qing 6b2097ccbb feat(inbox): archived notifications sub-view (MUL-3736) (#5518)
Adds an "Archived" sub-view to the Inbox, reachable from an entry at the
bottom of the main list, with per-row unarchive. Mirrors chat's archived
sub-view so the two surfaces share one mental model.

Backend:
- GET /api/inbox/archived and POST /api/inbox/{id}/unarchive. Kept off the
  existing GET /api/inbox so installed clients keep their contract and the
  unbounded archive never rides along with the main list.
- The archived query excludes any issue that still has an active row. Archiving
  is issue-level, so a new notification on an archived issue leaves old archived
  rows beside a fresh active one — without the guard the issue renders in BOTH
  lists. The exclusion lives in SQL so neither list depends on the other's cache.
- Unarchive is issue-level (mirroring archive) and leaves `read` untouched, so a
  restored unread item raises the unread badge again.
- v1 ships no pagination: LIMIT 200, newest-first, so truncation drops the
  oldest rows and never hides a group's newest one.
- inbox:unarchived event, fanned out to the recipient like the other personal
  inbox events.
- Two CONCURRENTLY-built indexes; inbox_item previously had none covering
  workspace/archived/created_at.

Frontend:
- Separate TanStack cache per list; every inbox event invalidates the workspace
  prefix, since any of them can move an item across the boundary.
- View persisted as ?view=archived, so refresh, back/forward, and the mobile
  detail-back all return to the list the user was in.
- Batch actions stay main-view only — they archive from the MAIN inbox, so
  offering them over the archived list would do the opposite of what it reads.
- Mobile subscribes to inbox:unarchived (its list gains the restored row); its
  own archived view remains follow-up.

Known debt: no pagination, so an archive past ~200 rows is truncated silently
in the UI; the entry's count is the deduplicated count of the rows returned.

Verified: pnpm typecheck/test/lint (0 errors), go build/vet, Go inbox suite
against a real Postgres, migrations up+down, and EXPLAIN confirming both new
indexes serve the query.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-16 14:58:42 +08:00

167 lines
7.1 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"
// 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)
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"
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"
// 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"
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"
// 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"
EventDaemonRuntimeProfilesChanged = "daemon:runtime_profiles_changed"
EventDaemonWorkspacesChanged = "daemon:workspaces_changed"
// 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"
// 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"
)