본문으로 건너뛰기

AA · 확장 후 — 애플리케이션 구조

갱신일 2026-08-12 · 기준: 확장 개요 · 현재 판 AA 이 문서가 갖는 것: 서비스 경계, 의존 방향과 그 강제 장치, 모듈 배치, 회의 종료 파이프라인의 상태 기계, 계약 사슬. 여기 없는 것: 화면(IA) · 프로토콜과 인증(SA) · AWS 리소스(CA) · 저장소 소유권과 삭제 정책(DA) · 물리 스키마(ERD) · 그래프 스키마(온톨로지) · 실시간 게이트(실시간).

⚠︎ 이건 계획이지 현황이 아니다. 지금 서 있는 것은 현재 판 AA가 갖는다.

1. 서비스 다섯 — 왜 이 선에서 갈랐나

경계를 가르는 기준은 도메인이 아니라 "따로 껐다 켰다 배포할 이유가 있나" 하나다.

서비스가른 이유그래서 지는 제약
web브라우저 능력(마이크·AudioWorklet)이 필요하다영속 저장소를 두지 않는다
server권한 판정과 도메인 상태가 한 곳에 있어야 감사와 잠금이 성립한다public edge · 오디오 릴레이 · 파이프라인 오케스트레이션을 혼자 진다
ai초 단위로 느리고 실패율이 다르다202로 끊고 callback. 무상태(D13)
speechGPU · 분 단위 배치 · 평소 0대. 수명이 다른 넷과 다르다도메인을 모른다. 오디오 in, 구간·임베딩 out
mcp기계가 부른다. 호출 패턴·인증·폭주 양상이 사람과 다르다DB에 안 붙는다. server REST만 쓴다
버린 대안왜 버렸나
speech를 ai에 넣기ai는 상시 떠 있는 IO 대기, speech는 간헐 GPU 점유다. 합치면 GPU 값을 24시간 낸다. 그리고 45분 오디오 배치 하나가 같은 프로세스의 채팅을 굶긴다 — ai는 초 단위 SLO, speech는 분 단위다
mcp를 server에 넣기기계 클라이언트는 루프를 돈다. 같은 프로세스면 MCP 폭주가 회의 중인 사람의 WebSocket을 끊는다. 격리가 이 서비스의 존재 이유라서 격리를 빼면 서비스도 뺀 것과 같다

전사 릴레이는 아직 server 안에 둔다

오디오가 브라우저 → server → Soniox 로 흐른다(D1). 릴레이를 지금 떼지 않는 이유는 하나다 — 1만 구간에서는 인스턴스 수로 흡수된다. 동시 50 스트림 = 3.2MB/s, IO 바운드다.

분리 조건을 수치로 박아둔다. 하나라도 걸리면 heymoa-transcription 을 뗀다(heymoa-transcription-worker 저장소가 이미 있다).

#조건수치왜 이 수치
1인스턴스당 동시 스트림50 초과sendBufferSize 4MB × 50 = 200MB. 버퍼가 도메인 작업량보다 커지는 지점
2릴레이가 REST를 민다REST p99가 릴레이 없는 시각 대비 2배, 5분 지속한 프로세스에 둔 대가가 사용자에게 보이기 시작하는 선
3egress월 3TB 초과1만(0.95TB)과 10만(9.5TB) 사이. 릴레이 노드를 대역폭 기준으로 따로 고를 값어치가 생긴다

세 값 다 추정이다. 1번은 확장 개요 14번(릴레이 실측)이 들어오면 갈아엎는다. 조건을 수치로 안 적으면 "언젠가"가 된다.

2. 의존 방향 — 규칙 다섯

문서로 지키는 규칙은 6개월 안에 깨진다. 규칙마다 설정·크레덴셜·스키마 중 하나로 강제한다. 셋 다 안 되면 규칙을 포기하고 포기했다고 적는다.

#규칙강제 장치종류
1web은 /internal/** 를 모른다openapi3-server.yml 미러에서 /internal 경로를 빌드 태스크가 지운다. 지금은 수작업이다(현재 판 AA §7)설정
2ai는 refresh token을 갖지 않는다ai 태스크 role에 토큰 저장소 접근이 없다. runtime context로 단기 토큰만 내려간다크레덴셜
3speech는 도메인을 모른다openapi3-speech.yml 요청 스키마에 noteId·workspaceId·userId 필드가 없다. jobId · 오디오 URL · sampleRate스키마
4mcp는 DB에 직접 붙지 않는다mcp 태스크에 DB 시크릿을 주지 않고, 보안그룹에서 RDS·Neo4j 경로를 막는다크레덴셜 + 네트워크
5ai는 Neo4j를 읽기만 한다Neo4j에 ai 전용 읽기 전용 롤 계정을 만든다크레덴셜

3번이 제일 강하다. 필드가 계약에 없으면 코드가 못 쓴다. 그리고 새 규칙 넷은 인프라 층에서 막히므로 CI가 없어도 성립한다 — heymoa-server의 CI가 과금 문제로 멈춰 있는 상태에서 이게 중요하다. 현재 판의 비대칭(ai만 import-linter, 나머지는 문서와 리뷰)이 여기서 뒤집힌다.

3. 회의 종료 파이프라인 — 단일 잡에서 상태 기계로

지금 분석은 한 방이다: 잡 생성 → ai 디스패치 → callback 영속화 → 워치독. 단계가 다섯이 되면(D10)이 패턴이 안 늘어난다.

3.1 상태값

단계 행 하나에 상태 하나. 파이프라인 전체 상태는 저장하지 않고 단계 행에서 파생한다 — 두 곳에 쓰면 반드시 어긋난다. 단계 코드와 실행 주체는 3.3에 있다.

상태는 다섯뿐이다. CANCELED 를 따로 두지 않고 SKIPPED + skipReason 으로 쓴다 — 노트 삭제(S6)도 "남은 단계 SKIPPED(note_deleted)" 다. enum 하나보다 사유 문자열 하나가 싸다.

3.2 멱등 키 — 재시도와 늦은 callback을 같은 장치로 막는다

무엇막는 것
디스패치PgSingleFlight advisory lock (noteId, stage)새 장치가 아니다. 지금 쓰는 것의 키만 늘린다두 인스턴스가 같은 단계를 동시에 큐에 넣는 것
callback 수용stageRunId — 시도마다 새로 발급하는 TSID죽은 시도가 뒤늦게 보낸 callback. 현재 행의 값과 다르면 버린다
① 오디오S3 객체 키 audio/{noteId}/{sessionId}.pcm멀티파트 완료를 두 번 불러도 같은 객체
② 결과 쓰기(noteId, stageRunId) 로 기존 화자 구간을 지우고 다시 쓴다재시도가 구간을 두 배로 만드는 것
⑤ 투영noteId 기준 MERGE그래프 중복 노드

3.3 단계별 재시도와 타임아웃

단계코드주체시도 상한백오프
① 오디오 확정AUDIO_FINALIZEserver310s · 30s
② 화자 분리DIARIZEspeech (GPU)330s · 2m · 8m
③ 화자 매칭SPEAKER_MATCHserver35s 고정
④ 요약SUMMARIZEai330s · 2m
⑤ 그래프 투영GRAPH_PROJECTserver51m · 5m · 15m · 1h

타임아웃 값은 이 표에 없다. 단계별 타임아웃은 시간 상수라 SA §8이 갖는다. 여기는 시도 상한과 백오프만 갖는다. ② 만 값이 아니라 식(오디오 길이 × 계수)인 이유는 하나다 — 고정값은 2시간 회의에서 무조건 틀린다. 계수는 미확인(§10)이다.

⑤ 의 상한이 제일 후하고 백오프가 제일 긴 이유: 파생이라 늦어도 손해가 없고, 재시도 폭풍이 정본 쓰기를 밀면 안 된다. ④ 는 사용자가 화면에서 기다리므로 빨리 포기하고 실패를 보여주는 편이 낫다.

Spot 중단은 시도 횟수에 세지 않는다. 우리 잡의 결함이 아니다. 재큐는 SQS가 하고(§4), attempts 는 그대로 둔 채 interrupt_count 만 올린다(ERD §4). 무한 루프만 막으려고 중단 재시도에 별도 상한 5회를 둔다. 상한값은 추정이다.

DLQ로 간 메시지도 단계 행을 내린다. 아무도 안 내리면 행이 RUNNING 에 남아 워치독 타임아웃까지 방치되고, 그동안 화면은 "누가 말했는지 정리 중"을 계속 보여준다.

maxReceiveCount 5(CA §7)와 여기 시도 상한 3은 다른 축이다앞은 인프라 재전달 횟수, 뒤는 잡 실패 횟수다. 같은 수로 맞추지 않는다 — 맞추면 Spot 세 번이 잡 실패 세 번으로 보인다
DLQ 소비자를 둔다해당 단계를 FAILED(dlq) 로 내리고 last_error_codereceiveCount 를 남긴다. 인프라가 몇 번 다시 줬는지가 사후에 필요한 유일한 숫자다
후속 단계는 3.4를 그대로 탄다② 가 FAILED(dlq) 면 ③ 은 SKIPPED(no_segments), ④ 는 화자 없이 돈다. DLQ 전용 분기를 따로 만들지 않는다

3.4 건너뛰기 — 의존 하나만 약하다

선행 → 후속강도실패 시
① → ②강함FAILED → ②③ SKIPPED(no_audio)
② → ③강함FAILED → ③ SKIPPED(no_segments)
②③ → ④약함화자 없는 요약이 요약 없는 것보다 낫다
④ → ⑤강함FAILED → ⑤ SKIPPED(no_items)

약한 의존에는 대기 상한이 붙는다. ④ 는 ②③ 을 최대 15분(추정) 기다리고, 넘으면 화자 없이 시작하면서 ②③ 을 SKIPPED(wait_timeout) 로 내린다. ②③ 이 늦게 끝나도 ④ 를 다시 돌리지 않는다 — 재요약은 사용자가 버튼으로 명시할 때만. 사람이 이미 읽은 요약이 소리 없이 바뀌는 것은 기능이 아니라 버그로 신고된다.

조건부 건너뛰기 하나 더: 조직에 voice print 등록자가 0명이면 ③ 을 호출하지 않고 SKIPPED(no_voiceprint). 대조할 대상이 없는데 대조를 부르지 않는다.

3.5 부분 커밋 — 결과와 단계 상태를 같은 트랜잭션에 쓴다

커밋 지점쓰는 것화면에 나타나는 것
① 후오디오 객체 키 · 길이
② 후화자 구간화자 1~4 가 전사에 붙는다
③ 후세그먼트의 화자 라벨 (APP-233이 여는 축)이름이 붙는다
④ 후요약 · 결정 · 액션 아이템 (정본)요약 카드
⑤ 후그래프 투영 (파생)연결된 과거

결과만 커밋되고 상태가 RUNNING 에 남으면 워치독이 다시 돌리고, 그러면 3.2의 멱등 키가 유일한 방어선이 된다. 방어선을 하나로 두지 않는다.

3.6 S1을 관통시키면

2026-08-12 10:45, 민손 팀 / PRO-1 실시간 STT 회의가 끝난다.

시각단계일어나는 일
10:45:02SUCCEEDED릴레이가 이미 멀티파트로 쌓고 있었으므로 완료 호출만. 45분 · 약 86MB(Int16 PCM · CA §8)
10:45:03PENDING큐 적재. GPU 태스크 기동 대기
10:49SUCCEEDED화자 4명 검출 → 화면에 화자 1~4
10:49SUCCEEDED김민수·이서연·박준호 매칭. 최유진은 후보 없음 → 화자 4 유지
10:52SUCCEEDED요약 · 결정 2건 · 액션 아이템 3건
10:52SUCCEEDEDNeo4j 투영. SUPERSEDES 후보 1건(미확인)
11:10김민수가 화자 4 → 최유진 교정. 파이프라인을 다시 돌리지 않는다

마지막 줄이 설계다. 교정은 speaker 모듈의 UPDATE 이지 파이프라인 재실행이 아니다. 교정 하나가 GPU 잡을 부르면 사람이 교정을 안 하게 된다.

오케스트레이터는 server 다. speech·ai가 다음 단계를 부르면 파이프라인 지식이 세 저장소에 흩어지고, 순서를 한 번 바꾸는 데 세 저장소를 고쳐야 한다. speech·ai는 자기 일만 하고 callback으로 돌려준다 — 지금 analysis 가 하는 모양 그대로이고 단계 수만 늘었다. 워크플로 엔진(Temporal·Step Functions)은 들이지 않는다. 단계가 열을 넘거나 사람 승인이 중간에 끼면 그때 다시 본다.

4. S4 — Spot 중단을 파이프라인으로 추적한다

10:45 정각, 12개 회의가 동시 종료(1만 규모 피크). 그중 7번 잡이 Spot 중단을 맞는다.

시각7번 잡무슨 일
10:45:03PENDING12개 전부 큐에. speech 태스크 0대
10:45:10PENDING태스크 2개 기동. 콜드스타트 4분
10:49:20RUNNING · 시도 1 · stageRunId=A태스크 B가 집어감
10:51:40RUNNINGSpot 2분 경고. speech가 SIGTERM을 받는다
10:51:41PENDING · 시도 1 유지speech가 처리 중 메시지에 ChangeMessageVisibility(0) 을 걸어 큐로 즉시 돌려보내고 죽는다(CA §7). INTERRUPTED callback도 같이 보내지만 그건 계측·화면용이고, 재큐는 그걸 안 기다린다
10:51:45RUNNING · stageRunId=B살아 있는 다른 태스크가 집어간다. 콜드스타트를 다시 겪지 않는다
10:55SUCCEEDED → ③사용자 화면은 그동안 "누가 말했는지 정리 중"
10:58SUCCEEDED대기 상한 15분 안(10:45+15). 요약은 예정대로 나온다

여기서 실제로 일하는 장치는 셋이다. stageRunId — 태스크 B가 죽기 직전 결과를 늦게 보내도 현재 행은 stageRunId=B 라 A의 callback이 버려진다. Spot 중단을 시도로 안 세는 것 — 안 그러면 Spot 세 번에 사용자 잘못 없이 FAILED 가 난다. ④ 의 15분 대기 상한 — 재시도 한 번은 흡수하고 두 번 넘게 밀리면 화자 없이 요약을 내보낸다.

ChangeMessageVisibility(0) 조차 못 부르고 죽을 수 있다. 2분 경고 없이 사라지거나 네트워크가 먼저 끊기는 경우다. 그러면 visibility timeout이 만료되며 SQS가 같은 메시지를 다시 주고, 그것도 안 되면 워치독이 타임아웃에서 잡는다. 느린 경로가 있으므로 빠른 경로는 최적화이지 정확성 조건이 아니다. INTERRUPTED callback은 어느 경로에도 안 낀다 — 화면에 "재시도 중"을 띄우고 interrupt_count 를 세는 용도다.

5. 워치독 — 인스턴스마다 도는 문제

AnalysisWatchdogScheduler·ToolApprovalWatchdogScheduler 는 1분 주기다. 앱이 2대가 되는 순간 둘 다 돈다. 잡 중복은 PgSingleFlight 가 막지만 틱 자체가 중복이고, 대가는 셋이다 — DB 스캔이 인스턴스 수만큼, 타임아웃 판정이 경합, 실패 알림이 N 배.

해법: 틱 진입에 pg_try_advisory_lock(<스케줄러별 고정 키>). 잡으면 돌고 못 잡으면 그 틱을 건너뛴다. 끝나면 푼다.

리더를 뽑지 않고 틱마다 경합시킨다. 리더 상태를 저장하지 않으므로 임차 갱신도 만료 처리도 없고, 상태가 없으면 틀릴 것도 없다. 락을 쥐고 있지 않으므로 리더 태스크가 좀비여도 아무도 못 도는 상태가 안 된다. 세션 락이라 태스크가 죽으면 커넥션과 함께 풀린다 — 별도 정리가 없고 새 장치도 없다.

S5 — 14:00 배포 중이면: 리더였던 태스크가 종료되면서 락이 풀리고 다음 틱(≤60초)에 다른 태스크가 잡는다. 최악 60초 공백이고 워치독 타임아웃은 분 단위라 영향이 없다. 진행 중 회의 3건의 스트림은 워치독과 무관하다.

버린 대안
ShedLock 도입advisory lock이 이미 있는데 의존을 하나 더 늘린다
스케줄러 전용 인스턴스 1대배포 형상이 갈리고 그 1대가 SPOF가 된다
EventBridge Scheduler → ECS task인프라로 나가면 로컬에서 안 돈다. 스케줄이 코드에서 사라진다

6. 모듈 — 어디에 무엇이 사나

heymoa-server신규소유하는 것
transcription전사 세션 수명주기 · 세그먼트 · 오디오 릴레이 + S3 멀티파트 기록
pipeline (← analysis)개명단계 상태 기계 · 디스패치 · callback 수용 · 워치독 (§3)
speaker🆕화자 구간 · 매칭 판정 · 사람 교정 · 노트 스코프 이름표 · voice print 등록/삭제
graph🆕Postgres → Neo4j 투영과 재생성. 그래프 쓰기는 여기서만온톨로지
apitoken🆕PAT 발급 · 범위 · 만료 · 폐기 · 사용량 집계
agentchat채팅 영속화 · ai SSE passthrough · 도구 승인 상태 기계
realtimeagent🆕30초 틱 · 싼 게이트 · ai 호출 · 개입 카드 영속화

transcriptionpipeline 이 갈리는 선은 회의가 끝나는 시각이다. 그 전은 스트림, 그 후는 배치다. 재시도 모델도 지연 예산도 다르다.

heymoa-ai소유heymoa-speech 🆕소유heymoa-mcp 🆕소유
analysis요약·추출. 화자를 입력으로 받는다diarizepyannote 실행 — 구간만tools도구 정의 · 응답 모양 변환
chat채팅 그래프embed구간별 화자 임베딩authPAT 검증(server 위임) · 범위 강제
retrieval 🆕벡터 검색 + 그래프 순회를 한 곳에서둘뿐이다. 대조 모듈을 두지 않는다limit토큰별 분당·일당 상한

retrieval 을 쪼개지 않는 이유: 벡터 경로와 그래프 경로가 흩어지면 같은 질문에 답의 근거가 갈린다.

매칭 임계값을 GPU 워커에 넣지 않는 이유

③ 대조는 server의 speaker 가 한다. speech가 주는 것은 구간별 임베딩뿐이다. speaker 가 그 임베딩을 조직 스코프 voice print와 대조해 { segmentId, candidates: [{ voicePrintId, score }] }자기 안에서 만들고, 누구인지도 거기서 정한다. voice print를 읽는 경로는 그 하나뿐이다(DA §3).

임계값은 제품 판단이다"틀린 이름을 붙이는 것"과 "이름을 안 붙이는 것" 중 무엇이 덜 나쁜가는 신호 처리 문제가 아니다
배포 단위가 다르다값이 GPU 이미지 안에 있으면 숫자 하나 바꾸는 데 이미지를 다시 굽는다. 배포가 필요한 값은 굳는다
규칙 3이 깨진다판정하려면 조직·사용자를 알아야 하고, 그 순간 speech가 도메인을 안다

speaker 는 임계값 둘을 프로퍼티로 갖는다 — τ_auto(넘으면 자동 지정), τ_suggest(넘으면 후보 제시, 사람이 고름). 둘 다 못 넘으면 화자 N 으로 남는다. 값은 미확인이다(§10).

  • S1 — 최유진은 voice print 미등록이라 후보 자체가 안 나온다.
  • S2 — 게스트 2명도 후보가 없다. 김민수가 화자 5 = 이정훈(외부) 를 지정하면 speaker노트 스코프 이름표로 쓴다. voice print를 만들지 않으므로 다음 회의에 따라오지 않는다.
  • S7 — 최유진이 voice print를 지우면 speaker 가 임베딩을 즉시 지우고 이후 ③ 에서 후보로 내지 않는다. 과거 노트의 라벨은 남는다 — 그건 파이프라인 산출물이지 voice print가 아니다. 저장소별 삭제 사슬은 DA가 갖는다.

실시간 에이전트가 server에 붙는 이유

D13 — ai는 무상태를 유지한다. 회의 세션 상태(최근 창·마지막 틱 시각·이미 띄운 카드)를 ai 프로세스에 두면 인스턴스 고정이 필요해지고 재시작이 안전하다는 성질이 깨진다. 세그먼트는 이미 server에 있으므로 30초 틱이 새 스트림 소비자를 만들지 않고, 카드는 회의 후 요약이 회수하는 도메인 데이터다.

버린 대안
ai에 회의 세션 액터를 둔다인스턴스 고정 필요. 재시작에 카드와 창이 날아간다. D13 위반
heymoa-agent 서비스를 판다세그먼트를 다시 스트리밍해야 하고 경계 하나에 계약 하나가 붙는다. 여섯 번째 서비스를 만들 이유가 없다
web에서 틱을 돈다클라이언트가 LLM 게이트를 정하면 원가와 개입 정책이 브라우저에서 위조된다

게이트 정밀도와 비용은 실시간이 갖는다.

운영 API 넷 — pipeline 이 갖는다

자동으로 못 낫는 경로가 넷 있다. 콘솔과 psql로 하면 누가 무엇을 되돌렸는지가 안 남으므로 pipeline 모듈에 API로 박는다.

경로무엇을 한다
POST /internal/v1/ops/pipeline/{noteId}/retry?stage=단계 하나를 PENDING 으로 되돌린다. 새 stageRunId 를 발급하므로 옛 시도의 callback은 버려진다
POST /internal/v1/ops/dlq/redriveDLQ 메시지를 원 큐로 되돌린다
POST /internal/v1/ops/outbox/{id}/reprocess세워둔(parked) 투영 outbox 행 하나를 다시 태운다
POST /internal/v1/ops/graph/reproject?workspaceId=워크스페이스 단위 재투영(온톨로지 §6)

전부 ADMIN + 감사 이벤트다. 그리고 화면을 안 만든다 — 운영자 화면을 만들면 권한 모델이 하나 더 생기고, 분기에 몇 번 누르는 버튼에 그 값을 낼 이유가 없다. 언제 무엇을 누르는지는 런북(CA §12)이 갖는다.

기능 플래그 — 축 셋과 평가 시점

세 문서가 이미 존재를 전제로 쓰여 있다 — 실시간 §10 "기능을 조직 단위로 끌 수 있어야 한다", IA §8 조직 정책 화면, CA §16 원가와 요금제. 소유는 여기다.

누가 끄나왜 있어야 하나
전역 kill우리LLM 공급자가 죽거나 원가가 튀면 즉시 끈다
organization 정책조직 관리자 (/o/{org}/policy)생체정보와 국외 이전을 조직이 거부할 수 있어야 한다
워크스페이스 코호트우리새 기능을 몇 팀에만 먼저 켠다

대상은 다섯이다 — voice_print_matching · realtime_agent · graph_projection · mcp_edge · summary_pii_names. 더 늘리지 않는다. 스무 개가 되면 어떤 조합이 실제로 도는지 아무도 모르게 되고, §8이 검증할 수 있는 형상도 아니게 된다.

평가는 회의 시작 시 1회, 그 회의 내내 고정한다. 중간에 값이 바뀌면 앞 20분은 카드가 뜨고 뒤 25분은 안 뜬 회의가 생긴다. 그 회의의 요약 입력이 반쪽이 되고, 나중에 "왜 이 회의만 카드가 없나"에 답할 방법이 없다. 그래서 회의 시작 시점의 값을 노트에 박아 둔다.

저장은 Postgres(ERD), 캐시 30초. LaunchDarkly 류를 들이지 않는다 — 플래그 다섯에 외부 의존과 월 구독을 붙이는 값이 아니고, 그 서비스가 느려지면 회의 시작이 같이 느려진다.

7. 계약 사슬 — 서비스가 늘면 처음으로 하위호환이 필요해진다

경계마다 계약이 하나씩 붙는다.

경계계약생성/수기원본 소유
web ↔ server (REST)openapi3-server.yml생성 (restdocs-api-spec)server 코드
web ↔ server (STOMP)asyncapi-web-server.yml생성 + 일부 수기전사·토픽은 server / 채팅 SSE는 이 파일
server ↔ aiopenapi3-ai.yml · asyncapi-server-ai.yml수기ai
server ↔ speech 🆕openapi3-speech.yml수기 — 작고 안 변한다speech
server ↔ Soniox공급자 스펙우리가 아니다
mcp ↔ server 🆕기존 REST 재사용새로 만들지 않는다
mcp ↔ 외부 🆕MCP 스펙 + 도구 스키마수기프로토콜은 우리가 아니고 도구 스키마는 우리다

mcp가 server 전용 API를 파지 않는 이유

S3 — 박준호가 IDE에서 heymoa.search_decisions(query="STOMP 브로커") 를 부른다. mcp는 PAT를 붙여 웹이 쓰는 그 검색 REST 를 부르고 응답을 도구 모양으로 바꿔 돌려준다. 결과는 Decision 인스턴스 1대 전제로 SimpleBroker 유지, 근거 2026-07-22 회의 31:07.

권한 판정이 한 벌전용 API를 파면 워크스페이스 스코프 체크가 두 곳에 생긴다. 한쪽만 고치면 §2 규칙 4가 문서 안에서만 살아 있게 된다
응답 모양은 mcp가 바꾼다어댑팅은 소비자 쪽에서 한다. server에 도구 전용 필드를 넣지 않는다
감사와 사용량이 한 곳사람 호출과 기계 호출이 같은 경로를 지난다

예외는 하나: 도구 하나가 REST를 5번 왕복해야 하면 server에 조회를 추가한다. 단 MCP 전용이 아니라 일반 API 로 만든다. 웹이 쓸 수 없는 모양이면 만들지 않는다.

지금은 동시 배포로 회피하고 있다

계약이 바뀌면 세 저장소를 같이 올린다. 인스턴스가 1대라 옛 판과 새 판이 공존하지도 않는다. 서비스가 다섯이 되면 이 회피가 세 곳에서 동시에 깨진다.

깨지는 곳
롤링 배포 중 같은 서비스의 두 판이 동시에 산다S5 그대로다. 옛 태스크가 진행 중 회의를 붙든 동안 새 태스크가 새 회의를 받는다. 지금은 1대라 이 문제 자체가 없었다
비동기 경계가 늘어난다② 잡을 옛 판이 보내고 callback을 새 판이 받는다. S4의 Spot 재시도면 간격이 10분 이상 벌어진다
mcp 클라이언트는 우리가 배포하지 못한다S3의 PAT는 90일 만료다. 도구 스키마를 바꿔도 그 90일 동안 옛 스키마로 부르는 IDE가 살아 있다

세 번째가 결정적이다. 앞의 둘은 배포창을 짧게 잡으면 완화되지만, 90일짜리 토큰을 든 외부 클라이언트는 완화할 수단이 없다.

필요한 것은 버전 번호가 아니라 하위호환 규칙 셋이다.

#규칙어기면
1필드 추가는 optional로 시작한다처음부터 필수로 넣으면 옛 발신자가 그날로 400을 맞는다
2필드 제거는 두 판에 걸쳐서 — 안 쓰기 → 한 판 뒤 지우기한 판에 지우면 아직 그 필드를 읽는 옛 판이 살아 있는 동안 값이 사라진다
3수신 측이 모르는 enum 값에 예외를 던지지 않는다새 값 하나가 옛 판 전체를 500으로 만든다. server의 ExposedEnumContractTest 가 이미 이 방향이다

/v2 를 파는 것은 마지막 수단이다. 경로를 가르면 두 벌을 굴려야 하고, 두 벌을 굴리면 권한 판정도 두 벌이 된다. 하위호환으로 못 가는 변경이 나오면 REST가 아니라 MCP 도구 이름에만 버전을 붙인다 — 그쪽만 우리가 통제 못 하는 클라이언트를 갖는다.

8. 무엇으로 검증하나

§7이 하위호환 규칙 셋을 정해놓고 무엇이 그 규칙을 깨뜨려 보는지가 없었다. heymoa-server의 CI는 과금 문제로 멈춰 있다(§2). 그래서 검증을 다섯으로 못박는다 — 이 다섯이 안 도는 상태로는 서비스를 다섯으로 늘리지 않는다.

#무엇을어떻게없으면
① 계약미러가 원본과 갈라지는 것openapi3-server.yml 미러 diff를 빌드 태스크가 깨뜨린다. MCP 도구 스키마는 90일 전 스키마 픽스처로 회귀 검사한다§7 세 번째 깨짐(90일 PAT를 든 IDE)은 여기서만 잡힌다. 배포 창을 아무리 좁혀도 안 잡힌다
② 멱등같은 일이 두 번 와도 한 번같은 stageRunId callback 2회 · 죽은 stageRunId callback 1회 · outbox 워커 2회 실행이 각각 no-op3.2의 멱등 키가 문서에만 있게 된다. 재시도가 화자 구간을 두 배로 만드는 것은 사람이 보고서야 안다
③ 단계 상태 기계건너뛰기가 3.4 표대로① 실패 · ② 실패 · ②③ 15분 초과 · 노트 삭제 네 조합을 표 그대로 돌린다한 조합만 어긋나도 행이 RUNNING 에 남고 화면이 영원히 "정리 중"에 선다
④ 그래프파생이 정본과 같은지재투영 후 노드·관계 수 = decisions + action_items 집계D7이 무너진다. 갈라진 것을 아무도 모르는 상태가 제일 나쁘다
⑤ 게이트 정밀도회의 중 개입이 맞는지전사 30건 골드셋으로 intervene:true 정밀도를 잰다원가와 오탐이 같이 튈 때 어느 쪽이 원인인지 못 가른다. 실시간 §13의 "싼 게이트 정밀도"가 이걸로 채워진다

①~④ 는 빌드에서 돌고 ⑤ 는 골드셋이라 사람이 돌린다. 정각 버스트 부하 테스트와 게임데이(Spot 강제 중단·Neo4j kill·브로커 kill) 는 환경 소유자인 CA §15가 갖는다.

9. 설계 전제

  • 오케스트레이션은 server 하나가 진다. 파이프라인 지식이 흩어지면 순서 하나 바꾸는 데 세 저장소를 고쳐야 한다.
  • 파이프라인 전체 상태를 저장하지 않는다. 단계 행에서 파생한다.
  • 약한 의존에는 대기 상한을, 강한 의존에는 건너뛰기를 준다. 약한 것은 ②③ → ④ 하나뿐이고, 그래서 화자 없는 요약이 나갈 수 있다.
  • 자동 재요약을 하지 않는다. 사람이 읽은 문서가 소리 없이 바뀌는 것은 버그다.
  • 규칙은 설정·크레덴셜·스키마로 강제한다. 리뷰로 지키는 규칙은 규칙이 아니라 희망이다.
  • speech와 mcp는 도메인을 모른다. 하나는 신호 처리기, 하나는 프로토콜 어댑터다. 둘 다 권한을 판정하지 않는다.
  • 제품 판단은 인프라 워커에 넣지 않는다. 바꾸는 데 배포가 필요해지는 순간 그 값은 굳는다.
  • mcp 전용 API를 만들지 않는다. 권한 판정을 두 벌로 만드는 가장 빠른 길이다.

10. 미확인

무엇지금 아는 것무엇이 걸려 있나
pyannote 처리 시간없다② 의 타임아웃 계수(SA §8)와 ④ 의 15분 대기 상한. 둘 다 지금은 추정
DLQ 소비자를 큐마다 두나 하나로 두나큐는 단계별로 나눈다(CA §7)하나로 두면 메시지에서 단계 코드를 읽어야 하고, 큐마다 두면 워커가 큐 수만큼 는다
voice print 임계값 τ_auto·τ_suggest없다§6 매칭 판정. 틀린 이름 vs 이름 없음의 교환비가 정한다
인스턴스당 동시 스트림 상한시간당 230MB 라는 것까지§1 분리 조건 1번의 "50" 이 근거 없는 수다
REST p99 기준선없다§1 분리 조건 2번을 판정할 방법이 없다
Spot 중단 빈도 (서울 g5·g6)없다§4의 중단 재시도 상한 5회가 충분한지
Neo4j 재생성 소요없다⑤ 가 진짜 파생인지, 실질적 정본인지가 갈린다
결정·액션 아이템 추출 정확도도메인 모델이 MVP2·3로 잡아 뒀다나쁘면 ⑤ 앞에 사람 승인 단계가 붙고, 그때 워크플로 엔진 판단을 다시 한다
SUPERSEDES 후보를 ④ 가 내나 ⑤ 가 내나정해져 있지 않다④ 면 LLM이, ⑤ 면 그래프 질의가 정한다. 원가와 정확도가 다르다
MCP로 내보낼 범위없다전사 전문인지 요약만인지가 §7 도구 스키마를 정한다