Перейти к содержимому

Доменные события

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? Навык устанавливается или обновляется вместе со своим вердиктом проверки цепочки поставок

Первый блок — собственный жизненный цикл 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

Все шесть несут 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
Событие Полезная нагрузка Когда возникает
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>. Ни то, ни другое не отображается на категорию уведомлений — они управляют интерфейсом в реальном времени, а не центром уведомлений. События членства кадрами не пересылаются: они инвалидируют кеш форвардера, а хосты сессий сбрасывают подписки удалённого участника на рабочее пространство.