Доменные события
Шина событий
Заголовок раздела «Шина событий»DomainEventBus — это внутрипроцессная широковещательная шина публикации и подписки. Издатели вызывают publish(event); подписчики получают типизированный поток on<T>(). Каждое событие реализует DomainEvent и несёт occurredAt.
Шина работает внутри cc_server. Там живёт каждый издатель и каждый живой подписчик. Ни один клиент эту шину не видит: сервер пересылает отобранное подмножество в виде кадров notifications/* JSON-RPC, отфильтрованных для каждого подписчика по членству в рабочем пространстве.
| Показатель | Количество |
|---|---|
| Конкретные классы событий | 54 |
Исходные файлы (packages/cc_domain/lib/core/domain/events/) |
14 (плюс domain_event_bus.dart) |
| Абстрактные базовые классы, которые никогда не публикуются напрямую | 1 (TaskLifecycleEvent, sealed-иерархия) |
Каждое событие из каталога ниже живёт в этом единственном каталоге; больше нигде доменные события не объявлены.
События рабочего пространства, агентов и репозиториев
Заголовок раздела «События рабочего пространства, агентов и репозиториев»| Событие | Полезная нагрузка | Когда возникает |
|---|---|---|
WorkspaceCreated |
workspaceId |
Рабочее пространство создаётся |
AgentRunCompleted |
agentId, runId?, workspaceId?, conversationId? |
Агент завершает запуск (успешно или с ошибкой) |
AgentAwaitingInput |
workspaceId, kind, summary, spaceId?, conversationId?, agentId?, agentName? |
Агент начинает ждать человека: одобрения, ответа на вопрос ask_user или учётных данных для остановленного запуска (kind — approval, question или credential) |
RepoAdded |
repoId, path, workspaceId |
Репозиторий регистрируется; запускает фоновую индексацию кода |
SkillUpdated |
workspaceId, slug, origin, computedHash, scanVerdict? |
Навык устанавливается или обновляется вместе со своим вердиктом проверки цепочки поставок |
События pull request и ревью
Заголовок раздела «События pull request и ревью»Первый блок — собственный жизненный цикл PR. Второй — наблюдение за созданными вами PR: сигналы о PR, который открыли вы, и каждый из них несёт forUserId, чтобы опрос направлял их человеку, которого они касаются, а не рассылал всем.
| Событие | Полезная нагрузка | Когда возникает |
|---|---|---|
PullRequestPublished |
prId, workspaceId, repoOwner, repoName |
Агент открывает PR |
PullRequestStatusChanged |
status, workspaceId, prId?, repoFullName?, prNumber? |
PR смержен, закрыт, открыт, переоткрыт или одобрен |
PrMerged |
prId, workspaceId, agentId |
PR смержен — узкий сигнал только о слиянии, сохранённый ради аналитики и уведомлений |
PrReviewRequested |
workspaceId, repoOwner, repoName, prNumber, prTitle |
GitHub запрашивает ревью у пользователя сервера (опрос уведомлений, review_requested) |
PrMentioned |
workspaceId, repoOwner, repoName, prNumber, prTitle |
Пользователя сервера упомянули в PR (опрос уведомлений, mention) |
ExternalPrMerged |
workspaceId, repoOwner, repoName, prNumber, prTitle, mergedByLogin? |
Внешне отслеживаемый (не агентский) PR смержен, что обнаружено опросом уведомлений GitHub; сопоставляется с категорией уведомлений prMerged |
ExternalPrDetected |
repoOwner, repoName, prNumber, prTitle, author, workspaceId? |
Неагентский PR найден через опрос — только для конвейера (управляет external_pr_welcome), уведомления не порождает |
PrHeadChanged |
repoOwner, repoName, prNumber, prTitle, previousHeadSha, headSha, workspaceId |
Head SHA PR сместился — автор сделал push |
ReviewBecameStale |
workspaceId, spaceId, repoOwner, repoName, prNumber, prTitle, reviewedHeadSha, headSha |
Завершённое ИИ-ревью больше не описывает PR, который оно проверяло. Срабатывает, только когда для заменённого коммита есть ревью, поэтому остаётся сигналом, а не пингом на каждый push |
Наблюдение за созданными вами PR
Заголовок раздела «Наблюдение за созданными вами PR»Все шесть несут workspaceId, repoOwner, repoName, prNumber, prTitle и необязательный forUserId.
| Событие | Дополнительная полезная нагрузка | Когда возникает |
|---|---|---|
PrMergeReadinessChanged |
ready, reason |
Созданный вами PR стал готов к слиянию — или перестал |
PrReviewDecisionChanged |
decision, reviewersRemaining, approverLogin? |
Ревьюер одобрил, запросил изменения или его одобрение было отозвано |
PrChecksStatusChanged |
failing, failingCheckName?, failingCheckUrl? |
CI стал красным или восстановился |
PrCommentMentioned |
commentId, authorLogin, bodyPreview, isReviewComment, threadId?, path?, line? |
Кто-то упомянул вас в комментарии к PR или в комментарии ревью |
PrThreadReplied |
commentId, authorLogin, bodyPreview, threadId?, path?, line? |
Кто-то ответил в ветке обсуждения ревью, в которой вы участвуете |
PrThreadResolved |
threadId, commentId?, path?, line? |
Кто-то закрыл ветку обсуждения ревью, в которой вы участвуете |
События сообщений
Заголовок раздела «События сообщений»| Событие | Полезная нагрузка | Когда возникает |
|---|---|---|
MessageReceived |
spaceId, messageId, senderName, contentPreview, isAgentMessage, workspaceId?, mentions, requestedByUserId?, senderUserId? |
Приходит новое сообщение |
SpaceCreated |
spaceId, workspaceId |
Пространство создаётся |
SpaceDeleted |
spaceId, workspaceId |
Пространство удаляется |
SpaceProvisioningChanged |
workspaceId, spaceId, status, step? |
Пространство входит в подготовку, доходит до шага (клонирование репозитория, выгрузка PR, настройка агента) или переключается на «готово»/«сбой» — чат-мост рассказывает об этом на своей карточке задачи |
События тикетов
Заголовок раздела «События тикетов»| Событие | Полезная нагрузка | Когда возникает |
|---|---|---|
TicketCreated |
ticketId, workspaceId |
Тикет создаётся |
TicketAssigned |
ticketId, ticketTitle, ticketBody?, ticketUrl?, assignedAgentId?, assigneeType, assignedTeamId?, workspaceId |
Тикет назначен субъекту или команде |
TicketCompleted |
ticketId |
Работа завершается (терминальное состояние) |
TicketFailed |
ticketId, errorMessage |
Агент терпит сбой (терминальное состояние) |
TicketCancelled |
ticketId |
Тикет отменён (терминальное состояние) |
TicketStatusChanged |
ticketId, workspaceId, from, to |
Любое изменение статуса |
TicketReassigned |
ticketId, workspaceId, fromAgentId?, toAgentId? |
Тикет переназначен |
TicketDetailsUpdated |
ticketId, workspaceId |
Изменяются заголовок, описание или приоритет |
События жизненного цикла задач
Заголовок раздела «События жизненного цикла задач»Единый поток жизненного цикла задач — один отправленный запуск агента, ключом которому служит идентификатор его журнала запуска. Каждое событие несёт taskId, монотонный в пределах задачи seq, workspaceId?, agentId? и phase; в строках ниже перечислены только дополнительные поля.
| Событие | Полезная нагрузка | Когда возникает |
|---|---|---|
TaskDispatched |
— | Задача передана бэкенду на выполнение |
TaskRunning |
— | Задача начала выдавать вывод |
TaskWaitingLocalDirectory |
lockedPath, holderTaskId? |
Задача припаркована — другой задаче принадлежит нужная ей блокировка пути на диске |
TaskProgress |
note? |
Контрольная точка хода выполнения |
TaskMessage |
messageType, content |
Потоковое сообщение (text/thinking/tool_use/tool_result/error); передаётся в фазе progress |
TaskCompleted |
summary? |
Задача успешно завершена |
TaskFailed |
errorMessage |
Задача завершилась с ошибкой |
TaskCancelled |
— | Задача отменена до завершения |
У перечисления TaskPhase есть также член queued, но событие очереди никто не публикует — класса TaskQueued нет. Первый наблюдаемый кадр задачи — TaskDispatched.
События конвейеров
Заголовок раздела «События конвейеров»| Событие | Полезная нагрузка | Когда возникает |
|---|---|---|
PipelineRunCompleted |
workspaceId, pipelineRunId, templateId |
Запуск завершается |
PipelineRunCancelled |
workspaceId, pipelineRunId, templateId |
Запуск отменён (пользователем или системой) — намеренная остановка, а не ошибка |
PipelineRunFailed |
workspaceId, pipelineRunId, templateId, errorMessage |
Запуск терпит сбой |
Публикуются только терминальные переходы. Ни PipelineRunStarted, ни события на каждый шаг нет: о ходе шага клиенты узнают из собственных строк запуска и из потока жизненного цикла задач того агента, которого шаг отправил, а не из шины.
События наблюдаемости
Заголовок раздела «События наблюдаемости»| Событие | Полезная нагрузка | Когда возникает |
|---|---|---|
ActivityLogged |
id, actorType, actorId?, action, entityType, entityId?, details?, workspaceId?, runId? |
Создана запись журнала аудита |
BudgetThresholdCrossed |
scopeType, scopeId, spentCents, budgetCents, isHardStop |
Расход превысил порог |
События идентичности и членства
Заголовок раздела «События идентичности и членства»| Событие | Полезная нагрузка | Когда возникает |
|---|---|---|
WorkspaceMemberAdded |
workspaceId, userId, role |
Пользователь вступает в рабочее пространство (погашение приглашения или добавление администратором) |
WorkspaceMemberRemoved |
workspaceId, userId |
Участник удалён — активные сессии этого пользователя в пределах рабочего пространства должны немедленно перепроверить доступ |
WorkspaceMemberRoleChanged |
workspaceId, userId, role |
Роль участника меняется |
Членство — это вся поверхность идентичности на шине. Создание пользователя и отзыв устройства происходят, но доменными событиями не публикуются — отзыв применяется напрямую к активным сессиям, а не проходит через шину.
События календаря и встреч
Заголовок раздела «События календаря и встреч»| Событие | Полезная нагрузка | Когда возникает |
|---|---|---|
CalendarAuthExpired |
workspaceId, accountEmail |
Refresh-токен OAuth подключённого аккаунта окончательно недействителен (публикуется один раз за эпизод отключения) |
MeetingStartingSoon |
workspaceId, eventId, title, startTime, meetingUrl? |
Событие календаря начинается в пределах настроенного окна опережения |
MeetingRecordingStopped |
workspaceId, meetingId, title, userNotes, transcript, summaryInstructions? |
Запись встречи завершается и запускает встроенный конвейер meeting_summary |
События rig
Заголовок раздела «События rig»| Событие | Полезная нагрузка | Когда возникает |
|---|---|---|
RigClosedEvent |
workspaceId, rigId, reason |
Rig исчез вместе с причиной закрытия; потребители реагируют только на backendFailure — на любую другую причину либо был запрос, либо её сопровождает RigReaped |
RigControlChanged |
workspaceId, rigId, controller? |
Человек взял rig под исключительный контроль или вернул его (controller равен null при освобождении — событие не фиксирует, кто отпустил управление) |
RigReaped |
workspaceId, rigId, reason, agentId? |
Система вернула себе машину: простой, TTL или вытеснение из-за нехватки памяти (сообщается как idleTimeout), вместе с управлявшим агентом |
Открытие rig событием не является. Загрузка завершается, и строка rig переключается в состояние «готов»; ничего не публикуется, поэтому подписаться на RigOpened не на что.
Поддомены, в которых нет ни одного события
Заголовок раздела «Поддомены, в которых нет ни одного события»Несколько подсистем, которые снаружи выглядят событийными, таковыми не являются. Они перечислены здесь потому, что их отсутствие — осознанное проектное решение, о котором стоит знать, прежде чем искать хук:
| Поддомен | Как вместо этого меняется состояние |
|---|---|
| Память | record_memory_fact и компания пишут и возвращают; извлечение читает. На факт или конфликт ничего не публикуется |
| Оркестрация | Предложение, его согласование и порождённый запуск конвейера — это строки и вызовы RPC |
| Документы планов | submit_plan записывает ревизию; согласование материализует оркестрацию. Ни то, ни другое не объявляется событием |
| Артефакты | publish_artifact / revise_artifact пишут ревизии напрямую |
| Согласования | Эскалация обрабатывается внутри службы процесса согласования |
| Синхронизация тикетов | Вендорский координатор подписывается на события тикетов выше; входящие вебхуки применяются напрямую |
| Обновление календаря | Синхронизация вставляет и обновляет события, а клиент перечитывает их; широковещательного оповещения об обновлении нет |
| Слияния worktree | Записываются в worktree_merge_log, откуда их и читают |
Подписчики
Заголовок раздела «Подписчики»Каждый подписчик ниже создаётся внутри runCcServer (или службы, которую он строит). Клиентских подписчиков нет.
| Подписчик | События, которые он потребляет |
|---|---|
WorkspaceSeeder |
WorkspaceCreated |
| Начальное заполнение сьюита eval (встроенное) | WorkspaceCreated |
| Подготовка пространства чата (встроенная) | SpaceCreated |
PipelineTriggerDispatcher |
Каждое DomainEvent; каждый включённый триггер отбирает по своему типу |
SubPipelineResumeListener |
PipelineRunCompleted, PipelineRunFailed |
PipelineCostRollupListener |
AgentRunCompleted |
BudgetEvaluationListener |
AgentRunCompleted |
MeetingSummaryReconciler |
PipelineRunCompleted, PipelineRunFailed, PipelineRunCancelled (только запуски meeting_summary) |
PipelineStepResumeListener |
AgentRunCompleted |
AgentRunTaskCompleter |
AgentRunCompleted |
GoalSupervisor |
AgentRunCompleted |
CheckerDispatchListener |
AgentRunCompleted |
TeamRoutingService |
AgentRunCompleted, TicketAssigned |
OrchestrationRunListener |
PipelineRunCancelled, PipelineRunFailed |
MultiVendorTicketSyncCoordinator |
TicketCreated, TicketAssigned, TicketReassigned, TicketStatusChanged, TicketDetailsUpdated |
WorktreeGcListener |
TicketCompleted, TicketCancelled, SpaceDeleted, PrMerged, PullRequestStatusChanged |
ChatBridgeService |
MessageReceived, SpaceProvisioningChanged |
ActivityLogPersister |
ActivityLogged |
RigEventListener |
RigControlChanged, RigReaped, RigClosedEvent — вставляет уведомление простым языком в полосу управления управляющего агента |
StaleReviewWatcher |
PrHeadChanged — публикует ReviewBecameStale, только когда для заменённого коммита есть завершённое ревью |
GitHubPrConversationBridge |
AgentRunCompleted — исходящие ответы в разговорах PR |
NotificationFeedRecorder |
Двадцать два класса событий уведомлений, перечисленные ниже |
RemoteEventForwarder |
Те же двадцать два, плюс TicketReassigned и каждое TaskLifecycleEvent. Три события членства лишь инвалидируют его кеш членства по рабочему пространству; кадрами они не пересылаются |
LocalRpcServer / RemoteRelayHost |
WorkspaceMemberRemoved — сбрасывает привязанные подписки этого пользователя на рабочее пространство; сокет остаётся открытым |
TicketAssigned — это сигнал аудита и уведомлений и триггер конвейера. Назначение агенту фиксирует владение и не запускает выполнение — диспетчера тикетов нет. Исключение — назначение команде (задан assignedTeamId): TeamRoutingService отправляет лидера команды.
TicketRemoteSyncHandler объявлен, но в продакшен-коде никогда не создаётся, поэтому сегодня он ни на что не подписан. Клиентский NotificationEventMapper создаётся только в собственном тесте — это остаток внутрипроцессного пути времён до разделения клиента и сервера. Живые всплывающие уведомления приходят из кадров, описанных ниже.
Сопоставление уведомлений
Заголовок раздела «Сопоставление уведомлений»На сервере NotificationFeedRecorder и RemoteEventForwarder превращают эти события в кадры notifications/* (notification_wire.dart). На клиенте RpcNotificationMapper → mapNotificationFrame превращает кадр в AppNotification.
Категорий уведомлений ровно семнадцать. Несколько событий отображаются более чем на один метод передачи, потому что кадр несёт признак перехода (сбой или восстановление), а категория несёт вопрос («что делает CI?»).
| Категория | Исходное событие | Метод передачи |
|---|---|---|
newMessage |
MessageReceived |
notifications/message_received (упоминает вас) |
agentRunCompleted |
MessageReceived |
notifications/message_received (is_agent_message, без упоминания вас) |
agentAwaitingInput |
AgentAwaitingInput |
notifications/agent_awaiting_input |
ticketAssigned |
TicketAssigned |
notifications/ticket_assigned |
ticketStatusChanged |
TicketStatusChanged |
notifications/ticket_status_changed |
pullRequestPublished |
PullRequestPublished |
notifications/pr_published |
prMerged |
PrMerged |
notifications/pr_merged |
prMerged |
ExternalPrMerged |
notifications/external_pr_merged |
prMentioned |
PrMentioned |
notifications/pr_mentioned |
prMentioned |
PrCommentMentioned |
notifications/pr_comment_mentioned |
reviewRequested |
PrReviewRequested |
notifications/pr_review_requested |
reviewStale |
ReviewBecameStale |
notifications/review_stale |
prMergeReadiness |
PrMergeReadinessChanged |
notifications/pr_ready_to_merge / pr_merge_blocked |
prReviewDecision |
PrReviewDecisionChanged |
notifications/pr_approved / pr_changes_requested / pr_review_dismissed |
prChecksStatus |
PrChecksStatusChanged |
notifications/pr_checks_failed / pr_checks_recovered |
prThreadActivity |
PrThreadReplied |
notifications/pr_thread_replied |
prThreadActivity |
PrThreadResolved |
notifications/pr_thread_resolved |
meetingStartsSoon |
MeetingStartingSoon |
notifications/meeting_starting_soon |
calendarAuthExpired |
CalendarAuthExpired |
notifications/calendar_auth_expired |
rigStatusChanged |
RigControlChanged |
notifications/rig_control_changed |
rigStatusChanged |
RigReaped |
notifications/rig_reaped |
rigStatusChanged |
RigClosedEvent |
notifications/rig_closed |
Кадр перехвата rig не доставляется обратно тому, кто взял управление,
освобождение не фильтруется (событие не фиксирует, кто отпустил управление), и
только закрытие с backendFailure порождает кадр rig_closed — остальные
причины либо запрошены, либо уже покрыты кадром сбора.
AgentAwaitingInput публикует AgentAwaitingInputPublisher: он следит за
появлением новых записей в реестрах одобрений и блокировок по учётным данным, а
о каждом вопросе ask_user ему сообщает AgentQuestionService. Одно ожидание —
одно событие; когда ожидание заканчивается, ничего не публикуется.
Пять категорий по созданным вами PR несут forUserId, поэтому они доходят до автора, а не до всех в рабочем пространстве.
RemoteEventForwarder дополнительно пересылает TicketReassigned как notifications/ticket_reassigned, а каждое TaskLifecycleEvent — как notifications/task_<phase>. Ни то, ни другое не отображается на категорию уведомлений — они управляют интерфейсом в реальном времени, а не центром уведомлений. События членства кадрами не пересылаются: они инвалидируют кеш форвардера, а хосты сессий сбрасывают подписки удалённого участника на рабочее пространство.