nomadcode/packages/contracts/notes/flutter-core-api-candidates.md
toki 488a2e6c6a feat(project-sync): 프로젝트 동기화 저장 경계를 추가한다
마일스톤 작업 생성 동기화를 진행하기 위해 Plane project별 active sync 설정을 Core DB에 저장하고 조회할 수 있는 persistence 경계와 검증 산출물을 함께 정리한다.
2026-06-06 18:56:37 +09:00

25 KiB

Flutter/Core API Contract Candidates

이 문서는 source schema가 아니라 Flutter-first 전환 중 공유 계약 후보를 모으는 compatibility note다. 실제 Go core 구현(services/core)과 향후 작성될 Flutter 클라이언트(apps/client) 간의 인터페이스 정합성을 보장하기 위한 임시 계약 후보이며, 단일 source-of-truth 스키마가 정의되기 전까지의 과도기적 스펙을 정의한다.

API 안정성 레벨 정의

  • 안정 (Stable): 인터페이스와 데이터 포맷이 고정되어 있어 즉시 연동에 사용할 수 있음.
  • 후보 (Candidate): 연동 후보 스펙으로, 실연동 과정에서 필드가 추가되거나 변경될 수 있음.

1. 내부 통신 원칙

1.1 기본 표준

  • 설명: NomadCode 내부 런타임 간 신규 통신은 proto-socket을 기본 표준으로 사용한다.
  • 안정성 수준: 후보 (Candidate)
  • 적용 대상:
    • apps/clientservices/core
    • NomadCode Core ↔ 내부 control/runtime adapter
    • 향후 NomadCode가 직접 소유하는 MCP/action runtime 표면
  • 예외 대상:
    • health/readiness처럼 외부 운영 도구와 curl smoke가 직접 확인해야 하는 endpoint
    • Plane/Jira/Mattermost/IOP/OpenAI-compatible Responses/A2A처럼 외부 provider 또는 외부 실행 표면이 요구하는 native protocol
    • public callback/webhook
    • 단순 정적 조회처럼 proto-socket 연결 비용이 과한 read-only endpoint
  • 예외 기록 규칙: REST/HTTP를 새로 추가하거나 유지할 때는 이 문서 또는 관련 domain rule에 예외 사유를 남긴다.

1.2 proto-socket Semantic Envelope 후보

  • 설명: 아래 JSON은 wire schema가 아니라 proto-socket message가 가져야 할 의미 필드 후보다. 실제 source schema는 proto-socket 계약이 안정화되면 별도 schema로 승격한다.
  • Envelope Shape 후보:
    {
      "protocol_version": "nomadcode.proto-socket.v1",
      "id": "msg-uuid",
      "correlation_id": "request-or-flow-id",
      "type": "request",
      "channel": "task",
      "action": "task.list",
      "actor": {
        "kind": "client",
        "id": "apps/client"
      },
      "auth": {
        "scheme": "basic",
        "token_ref": "runtime-secret-ref"
      },
      "payload": {},
      "error": null,
      "meta": {
        "connection_id": "conn-id",
        "timestamp": "2026-05-30T00:00:00Z"
      }
    }
    
  • type 후보:
    • request: 요청 메시지
    • response: 요청 성공 응답
    • event: 구독 또는 서버 발행 이벤트
    • error: 요청 실패 또는 channel-level 오류
    • heartbeat: 연결 생존 확인
    • auth: 연결 인증 또는 인증 갱신
  • error 후보 필드:
    {
      "code": "task.not_found",
      "message": "task not found",
      "retryable": false,
      "details": {}
    }
    

1.3 REST Compatibility Map 후보

  • 설명: 현재 REST API는 proto-socket rail 구현 전까지의 호환 표면이다. task 관련 신규 내부 기능은 proto-socket channel을 우선한다.
현재 REST/HTTP 표면 proto-socket 후보 유지/전환 기준
GET /healthz 없음 Public 운영 health check로 REST 유지
GET /readyz system.ready event 또는 request 후보 DB readiness와 curl smoke 때문에 REST 유지, 내부 UI에는 proto-socket 상태 이벤트를 추가 가능
POST /api/tasks task.create request client-core 내부 호출은 proto-socket으로 전환, REST는 smoke/compat 유지
GET /api/tasks task.list request client-core 내부 조회는 proto-socket으로 전환, REST는 smoke/compat 유지
GET /api/tasks/{id} task.get request client-core 내부 조회는 proto-socket으로 전환, REST는 smoke/compat 유지
POST /api/tasks/{id}/enqueue task.enqueue request + task.status.changed event client-core 실행 요청과 상태 반영은 proto-socket으로 전환, REST는 smoke/compat 유지
POST /api/integrations/plane/tasks workitem.plane.import 후보 Plane API 자체는 외부 provider REST이며, 내부 호출 표면은 proto-socket으로 추가 가능

1.4 Core Endpoint 후보

  • 기본 경로: /proto-socket
  • 인증: 기존 Core Basic Auth middleware 적용
  • Wire message: 초기 구현은 google.protobuf.Struct semantic envelope를 사용한다.
  • Heartbeat: proto-socket 기본 heartbeat를 사용하며 Core 설정 기본값은 interval 30s, wait 10s다.

1.5 Task Channel Actions (구현됨)

  • 설명: services/core/internal/protosocket가 REST task API와 같은 의미를 task channel의 proto-socket action으로 제공한다. REST는 smoke/compat로 유지한다.
  • 안정성 수준: 후보 (Candidate)
  • 채널: task
  • 요청/응답 action 표:
action request payload response type response payload
task.create POST /api/tasks JSON shape (title, source, payload, metadata?, external?) response { "id", "status", "external_provider", "external_id", "task": { …full task } }
task.list { "limit": 20 } (생략 시 0 전달, 기본 20/최대 100 bounding은 core workflow service가 담당) response { "tasks": [ { …full task } ] }
task.get { "id": "task-id" } response { "task": { …full task } }
task.enqueue { "id": "task-id" } response { "id", "status": "queued", "task": { …full task } }
  • task status event (서버 발행):
action type channel payload
task.status.changed event task { "id", "type": "task.running|task.completed|task.failed|task.canceled", "status", "title", "message", "attempt?", "reason?", "occurred_at?" }
  • scheduler가 내는 모든 task lifecycle event(running/completed/failed/canceled)가 notification fanout을 통해 broadcast된다. completed event의 Mattermost 알림 동작은 그대로 유지된다.

  • attempt (int), reason (string), occurred_at (ISO8601 string) 등의 workflow context가 payload에 포함된다.

  • error envelope code 표 (type: "error"):

code 의미 retryable REST 대응 status
task.invalid_input 잘못된 task 입력(ErrInvalidTaskInput) false 400
task.conflict 현재 상태에서 enqueue 불가(ErrTaskCannotBeEnqueued) false 409
task.not_found task 없음(pgx.ErrNoRows) false 404
task.invalid_payload envelope payload 디코드 실패 false (REST 없음)
internal.error 그 외 내부 오류 true 500
UNSUPPORTED_ACTION dispatcher에 등록되지 않은 action false (REST 없음)

1.6 Diagnostics 필드 후보

  • 설명: proto-socket 로그와 클라이언트 디버그 표면에서 연결과 메시지 흐름을 추적하기 위한 공통 diagnostics 필드 후보다. 확정 schema가 아니라 Core/Client 구현을 맞추기 위한 compatibility note로만 사용한다.
  • 안정성 수준: 후보 (Candidate)
  • 최소 공통 필드:
필드 위치 필수성 소비자 책임
connection_id envelope meta.connection_id, Core log attribute, Client debug surface 연결이 확립된 response/event/error에서 가능하면 포함 Core는 연결별 안정 식별자를 부여하고, Client는 표시/필터링에만 사용한다.
protocol_version envelope top-level, Core log attribute, Client debug surface 모든 proto-socket envelope에 포함 Core와 Client는 지원하지 않는 version을 compatibility 문제로 보고하되, secret이나 payload 원문을 함께 출력하지 않는다.
channel envelope top-level, Core log attribute, Client debug surface request/response/event/error에서 포함 dispatcher와 Client debug surface는 channel name을 그대로 노출하되 business payload를 함께 노출하지 않는다.
action envelope top-level, Core log attribute, Client debug surface request/response/event/error에서 포함 request 흐름과 최근 event/error를 추적하는 용도로만 사용한다.
error.code / error_code envelope error.code, Core log attribute, Client debug surface error envelope에서 포함, 성공 envelope에서는 생략 또는 null Core는 error code를 안정적인 문자열로 기록하고, Client는 최근 오류 요약에만 사용한다.
timestamp envelope meta.timestamp, Core log timestamp, Client debug surface Core가 발행하는 response/event/error에서 가능하면 포함 소비자는 시간 정렬과 최근 상태 표시에 사용하고 clock 차이를 프로토콜 오류로 단정하지 않는다.
  • Core log attributes: connection_id, protocol_version, channel, action, error_code, timestamp를 structured log attribute로 남긴다. error가 없으면 error_code는 생략하거나 null로 둔다.
  • Envelope meta: response/event/error는 가능한 경우 meta.connection_id, meta.timestamp를 포함하고, top-level protocol_version, channel, action과 함께 diagnostics 기준으로 소비된다.
  • Client debug surface: 연결 상태, connection id, protocol version, 최근 channel/action/error code/timestamp만 표시한다.
  • 노출 금지: auth token, Basic Auth header, secret, token_ref가 가리키는 실제 값, payload/result 원문, 외부 provider credential은 Core log와 Client debug surface에 기록하거나 표시하지 않는다.

1.7 새 내부 통신 추가 운영 체크

  • 신규 apps/clientservices/core 또는 내부 runtime 간 통신은 먼저 proto-socket channel/action으로 표현 가능한지 확인한다.
  • REST/HTTP를 새로 추가하거나 유지해야 하면 health/readiness, 외부 provider 호환 API, 운영 smoke/curl, public callback, 단순 정적 조회 중 어느 예외인지 이 문서 또는 관련 domain rule에 기록한다.
  • 새 channel/action은 envelope의 protocol_version, type, channel, action, correlation_id, payload, error.retryable 의미를 이 문서에 추가한다.
  • 클라이언트가 소비하는 request/response/event shape는 구현 전에 후보 표와 테스트 fixture 기준을 남긴다.
  • Core 구현은 기존 REST smoke/compat 표면을 제거하지 않고, 내부 UI/런타임 호출만 proto-socket 우선 경로로 전환한다.
  • 검증은 최소한 channel/action dispatch, auth boundary, error envelope, event broadcast 또는 reconnect 영향 중 해당되는 항목을 테스트로 고정한다.

2. 공통 및 인프라 API

2.1 GET /healthz

  • 설명: 서비스 헬스 체크.
  • 인증: 없음 (Public)
  • 안정성 수준: 안정 (Stable)
  • Response Shape (Status 200 OK):
    {
      "status": "ok"
    }
    

2.2 GET /readyz

  • 설명: 서비스 및 데이터베이스 준비 상태 체크.
  • 인증: Basic Auth
  • 안정성 수준: 안정 (Stable)
  • Response Shape (Status 200 OK):
    {
      "status": "ready"
    }
    
    (Status 503 Service Unavailable):
    {
      "error": "database is not ready"
    }
    

3. 태스크 관리 API (Tasks API)

3.1 POST /api/tasks

  • 설명: 새로운 태스크 생성.
  • 인증: Basic Auth
  • 안정성 수준: 안정 (Stable)
  • Request Shape:
    {
      "title": "Task Title",
      "source": "source_identifier",
      "payload": {},
      "metadata": {},
      "external": {
        "provider": "plane",
        "id": "external-task-id",
        "url": "https://...",
        "metadata": {}
      }
    }
    
  • Response Shape (Status 201 Created):
    {
      "id": "task-uuid-string",
      "status": "pending",
      "external_provider": "plane",
      "external_id": "external-task-id"
    }
    

3.2 GET /api/tasks

  • 설명: 태스크 목록 조회.
  • 인증: Basic Auth
  • 안정성 수준: 안정 (Stable)
  • Query Parameters:
    • limit (Optional, 기본값 20, 양의 정수)
  • Response Shape (Status 200 OK):
    [
      {
        "id": "task-uuid-string",
        "title": "Task Title",
        "source": "source_identifier",
        "status": "pending",
        "payload": {},
        "result": {},
        "error": null,
        "created_at": "2026-05-24T12:00:00Z",
        "updated_at": "2026-05-24T12:00:00Z",
        "external_provider": "plane",
        "external_id": "external-task-id",
        "external_url": "https://...",
        "external_metadata": {},
        "metadata": {}
      }
    ]
    

3.3 GET /api/tasks/{id}

  • 설명: 단일 태스크 세부 정보 조회.
  • 인증: Basic Auth
  • 안정성 수준: 안정 (Stable)
  • Response Shape (Status 200 OK):
    {
      "id": "task-uuid-string",
      "title": "Task Title",
      "source": "source_identifier",
      "status": "pending",
      "payload": {},
      "result": {},
      "error": null,
      "created_at": "2026-05-24T12:00:00Z",
      "updated_at": "2026-05-24T12:00:00Z",
      "external_provider": "plane",
      "external_id": "external-task-id",
      "external_url": "https://...",
      "external_metadata": {},
      "metadata": {}
    }
    

3.4 POST /api/tasks/{id}/enqueue

  • 설명: 태스크를 큐에 진입시켜 실행 대기 상태로 변경.
  • 인증: Basic Auth
  • 안정성 수준: 안정 (Stable)
  • Response Shape (Status 200 OK):
    {
      "id": "task-uuid-string",
      "status": "queued"
    }
    

4. 외부 연동 API (Integrations API)

4.1 POST /api/integrations/plane/tasks

  • 설명: Plane 워크아이템 정보를 기반으로 새로운 태스크 생성.
  • 인증: Basic Auth
  • 안정성 수준: 안정 (Stable)
  • Request Shape:
    {
      "workspace_slug": "my-workspace",
      "project_id": "project-uuid-string",
      "work_item_id": "work-item-id-string",
      "state_id": "state-uuid-string",
      "external_url": "https://...",
      "comment": "Optional comment"
    }
    
  • Response Shape (Status 201 Created):
    {
      "id": "task-uuid-string",
      "status": "pending",
      "external_provider": "plane",
      "external_id": "work-item-id-string"
    }
    

5. Roadmap / Work Item Sync 계약 후보

Important

이 절은 source schema가 아니라 Plane/Jira 같은 provider work item과 agent-roadmap Milestone item을 같은 구조로 유지하기 위한 sync domain 계약 후보입니다. 실제 구현 전까지는 compatibility note로만 사용합니다.

5.1 Sync Domain 책임 후보

  • 설명: Core 안의 sync domain은 provider adapter 위에서 동작하며, provider native API 세부가 아니라 Milestone/work item identity, revision, 변경 감지, 수렴 적용, conflict review gate를 소유한다. Milestone sync의 source of truth는 develop branch의 agent-roadmap이다.
  • 적용 대상:
    • Plane/Jira 상위 work item ↔ develop branch의 agent-roadmap Milestone
    • Plane/Jira 하위 work item ↔ develop branch의 Milestone 기능 Task item
    • agent-task/m-<milestone-slug> plan/code-review/completion event ↔ Milestone Task ↔ provider child work item
  • 비소유 대상:
    • Plane/Jira native DTO와 endpoint 세부
    • 사용자 승인 없는 archive/delete 강제 적용
    • provider별 custom field 전제 설계

5.2 Sync Identity 후보

  • 설명: 어느 쪽에서 변경이 발생해도 같은 Milestone item 구조로 찾고 비교하기 위한 provider-neutral identity shape다.
  • 후보 필드:
필드 설명
shape milestone 또는 task
roadmap_milestone_path agent-roadmap/phase/<phase-slug>/milestones/<milestone-slug>.md
roadmap_item_id Milestone 자체이면 파일 slug, Task이면 Milestone 안의 item-id
provider plane, jira 등 provider id
tenant provider workspace/site/account 식별자
project provider project 식별자
work_item_id provider work item id 또는 issue key
parent_work_item_id Task shape일 때 provider 상위 work item id
roadmap_revision roadmap 문서 또는 Core index가 본 revision
roadmap_branch source of truth branch. Milestone sync에서는 기본값 develop
provider_revision provider가 제공하는 updated timestamp/version/hash 후보
observed_at Core가 변경을 관측한 시각

5.3 Project Sync Configuration 후보

  • 설명: Milestone sync는 Plane/Jira provider project 단위로 묶이며, Core DB에 저장된 project sync 설정을 통해 provider project, git repository, 실제 작업 workspace를 해석한다.
  • 저장 위치: Core DB. source schema와 migration은 구현 시점에 확정한다.
  • 후보 필드:
필드 설명
sync_project_id Core 내부 project sync 설정 id
provider plane, jira 등 provider id
provider_tenant Plane workspace slug 또는 Jira site/account 식별자
provider_project_id Plane project id 또는 Jira project id/key
provider_project_name 표시용 provider project 이름
git_remote_url source-of-truth roadmap이 있는 git repository URL
git_default_branch source-of-truth branch. 기본값 develop
roadmap_root_path repository 안의 roadmap root. 기본값 agent-roadmap
workspace_id 실제 작업 workspace 식별자
workspace_path local/runner에서 실제 작업이 돌아갈 workspace path 후보
enabled sync 활성화 여부
settings provider별 확장 설정 JSON
created_at / updated_at 생성/수정 시각
  • 운영 원칙:
    • Plane/Jira work item intake와 projection은 반드시 project sync 설정을 통해 대상 git/workspace를 확정한 뒤 실행한다.
    • 같은 provider project의 Milestone/work item sync는 같은 active project sync 설정을 사용한다.
    • project sync 설정이 없거나 둘 이상이면 Milestone 생성 동기화를 진행하지 않고 사용자 설정 또는 운영자 조치가 필요하다고 보고한다.
    • 이 설정은 이후 Project settings UI/UX에서 확인/수정할 수 있어야 한다.

5.4 Sync Change Event 후보

  • 설명: provider webhook, provider polling, roadmap file/index scan, agent-task completion event를 같은 변경 입력으로 정규화한다.
  • proto-socket event 후보:
action type channel payload
roadmap_sync.changed event roadmap_sync { "source", "change_type", "identity", "fields", "actor?", "reason?", "observed_at" }
  • payload 후보 필드:
    • source: provider_webhook, provider_poll, roadmap_scan, agent_task_completion, manual
    • change_type: created, updated, status_changed, parent_changed, completed, archived, deleted
    • identity: 5.2의 Sync Identity shape
    • fields: title, description, status, labels, parent-child, order, review state 같은 변경 후보
    • actor: provider user, NomadCode actor, external agent 등 변경 주체
    • reason: 사용자 요청, completion event, scheduled reconciliation 등 적용 사유

5.5 Sync Action 후보

  • 설명: 외부 agent, Flutter UI, scheduler가 sync domain에 검사와 적용을 요청할 때 사용하는 action 후보다.
action request payload response payload
roadmap_sync.inspect { "identity?", "scope?", "source?", "dry_run": true } { "matches", "drift", "conflicts" }
roadmap_sync.apply { "identity", "change", "expected_revision?", "idempotency_key", "actor", "reason", "dry_run": false } { "applied", "identity", "roadmap_revision", "provider_revision", "conflict?" }
roadmap_sync.reconcile { "scope", "provider?", "since?", "dry_run": true } { "changes", "proposals", "conflicts" }
  • 공통 입력 원칙:
    • destructive 변경, archive, delete, 사용자 승인 상태 변경은 dry_run 또는 review gate를 우선한다.
    • expected_revision이 맞지 않으면 조용히 덮어쓰지 않고 conflict로 반환한다.
    • 같은 변경 재시도는 idempotency_key로 중복 적용을 피한다.
    • provider webhook이 없거나 누락될 수 있으므로 scheduler polling과 같은 action shape를 공유한다.
    • feature branch 또는 로컬 초안은 Plane/Jira Todo projection 대상이 아니며, develop branch에 반영된 roadmap만 sync 확정본으로 적용한다.

5.6 Sync Convergence 정책 후보

  • develop branch의 Milestone과 provider parent work item은 같은 목표, 범위 요약, 상태, review gate를 표현한다.
  • Milestone 기능 Task와 provider child work item은 같은 제목/상태/parent 관계를 표현한다.
  • roadmap 쪽 변경과 provider 쪽 변경은 모두 sync change event로 정규화한 뒤 Core action을 통해 적용한다.
  • 동시에 수정된 항목은 revision mismatch로 멈추고, 사용자 검토 또는 dry-run proposal로 올린다.
  • provider 상태는 가볍게 유지하고, agent 내부 실행 상태는 metadata/labels/comment로 투영한다.
  • Done/archive/delete처럼 되돌리기 어려운 변경은 사용자 승인 또는 명시 action 없이 자동 적용하지 않는다.

6. Workspace / Project Metadata 계약 후보

Important

Workspace/Project Metadata 상태: 일반 Workspace/Project metadata는 아직 완전한 source schema가 없으며 아래는 후보 필드 목록입니다. 단, Milestone sync에 필요한 provider project, git remote, workspace 설정은 5.3의 Project Sync Configuration 후보로 분리하고 Core DB 저장 대상으로 본다.

6.1 Workspace Metadata 후보 스펙

  • 설명: 에이전트 작업 공간(Workspace)에 대한 설정 및 상태 메타데이터.
  • 후보 필드:
    • workspace_id: 작업 공간 고유 식별자 (UUID)
    • workspace_slug: 작업 공간의 URL 친화적 식별자 (예: nomadcode-dev)
    • name: 작업 공간 표시 이름 (예: NomadCode Dev Workspace)
    • owner_id: 소유자 고유 식별자
    • created_at: 생성 시각 (ISO 8601)
    • updated_at: 최종 변경 시각 (ISO 8601)
    • status: 현재 상태 (예: active, archived, suspended)
    • settings: 작업 공간에 특화된 동적 설정 JSON 오브젝트 (예: 자동 스케줄링 옵션, 알림 채널 정보 등)

6.2 Project Metadata 후보 스펙

  • 설명: 작업 공간 내 세부 프로젝트(Project) 정보.
  • 후보 필드:
    • project_id: 프로젝트 고유 식별자 (UUID)
    • workspace_id: 해당 프로젝트가 속한 Workspace 식별자
    • name: 프로젝트 표시 이름 (예: Flutter client consolidation)
    • description: 프로젝트 세부 설명
    • provider: 연동 플랫폼 식별자 (예: plane, github)
    • external_project_id: 연동 플랫폼 측 프로젝트 ID
    • status: 프로젝트 상태 (예: active, paused)

7. 클라이언트 Integration 설정 계약 후보

Important

이 절은 apps/client의 integration boundary에서 host/plugin/transport 사이에 교환되는 설정값을 후보로 모은 것이다. 실제 source schema는 소비하는 core/client 경계가 안정화된 뒤로 미룬다. 관련 코드 경계: apps/client/lib/src/integrations/proto_socket/apps/client/lib/src/integrations/mattermost/.

7.1 proto-socket Endpoint 설정 후보

  • 설명: apps/client가 proto-socket transport에 의존할 때 사용하는 연결/하트비트 설정. 현재 구현은 proto_socket Dart 패키지(local path)를 끌어 쓰며 bootstrap에서 자동 연결하지 않는다.
  • 후보 필드:
    • host: 대상 서버 hostname
    • port: TCP/WebSocket 포트 (기본은 secure 값에 따라 443 또는 80)
    • secure: TLS 사용 여부 (boolean)
    • path: WebSocket path (기본 /)
    • heartbeat_interval_seconds: heartbeat 송신 주기 (기본 30)
    • heartbeat_wait_seconds: heartbeat 응답 대기 한계 (기본 60)
    • enabled: bootstrap 시 자동 연결 활성화 여부 (기본 false)

7.2 Mattermost Push Host 책임 경계 후보

  • 설명: Mattermost push 통합에서 apps/client host가 plugin adapter에게 위임/공급해야 하는 데이터와 콜백. plugin adapter는 platform 측 plugin singleton에 격리되어 있고, 그 외 코드는 모두 MattermostPushClient 인터페이스에만 의존한다.
  • 호스트 소유 필드/콜백:
    • server_url: Mattermost 서버 base URL (auth 핸드오프, signing key 저장에 함께 사용)
    • server_identifier: 선택 필드. FCM payload의 server_idserver_url로 역해석해야 하는 multi-server host에서 plugin의 setAuthToken(..., identifier:)로 함께 전달
    • auth_token: 로그인 응답에서 받은 인증 토큰 (plugin의 setAuthToken으로 전달)
    • signing_key: 서버 config의 AsymmetricSigningPublicKey (plugin의 setSigningKey로 전달)
    • device_token: FCM 디바이스 토큰 (plugin의 getDeviceTokenonDeviceTokenReady 콜백으로 동기화)
    • on_navigate_to_channel(server_url, channel_id): notification open → 채널 라우팅 콜백
    • on_navigate_to_thread(server_url, root_id): notification open → CRT 스레드 라우팅 콜백
    • notification_stream: app-level UI(snackbar 등) 소비를 위한 broadcast stream