Pantheon 페르소나 도구·세션 아키텍처 (전략 수립용)
Summary
페르소나(jini, wansu, raphael, nano, jarvis, asurada)는 각자 독립 Slack 봇 인스턴스로 부팅되고, 메시지가 들어오면 Pantheon bridge가 라우팅 → 세션 복원 → LLM 호출 → Slack 응답 순으로 처리한다. MCP 도구는 페르소나가 띄우는 LLM 프로세스의 환경에 정의된 것만 보이며, 사용자(hangman) Claude Code 본 세션의 MCP 연결과는 완전히 분리된다. 본 보고서는 이 구조를 5개 레이어로 분해해 도구 관리 전략 의사결정 근거를 만든다.
Context
왜 쓰는가
콜로세움 트래커 케이스에서 Jini는 Notion 접근 불가, Wansu는 reconnect 후 접근 가능이라는 비대칭이 관찰됐다. 단발 디버깅으로는 풀 수 있지만, 앞으로 도구를 어떻게 늘리고 운영할지 결정하려면 시스템 구조 자체를 이해해야 한다.
따라서 본 보고서는 이번 사고 해명이 아니라 향후 도구 관리 전략 의사결정용 인프라 보고서다.
표기 규약
- 확정 사실: 소스 파일·라인 인용으로 검증된 것 (
file:line형식) - 추정: 관측 정황 기반 가설 (소스로 직접 확인 못한 부분)
- 모름: 명시적으로 미확인
전체 흐름 한 장
flowchart TB
U["사용자 (Slack)"] -->|메시지| SL[Slack Cloud]
subgraph Pantheon["Pantheon (단일 Python 프로세스: bridge.py)"]
direction TB
SH1["Bolt App #1 (jini bot)"]
SH2["Bolt App #2 (wansu bot)"]
SH3["Bolt App #3 (raphael bot)"]
SH4["Bolt App #N (...)"]
ROUTER["라우터<br/>parse_mention + auto_channels + northstar gate"]
SESSION["세션 매니저<br/>persona_sessions + persona_memory"]
DISPATCH["ask_llm dispatcher"]
PROV_C["providers/claude.py<br/>subprocess: claude -p --resume"]
PROV_X["providers/codex.py<br/>OpenAI API"]
PROV_G["providers/gemini.py"]
end
subgraph LLM_PROC["LLM 프로세스 (페르소나마다 새로 spawn)"]
direction TB
LLM["Claude CLI / Codex / Gemini"]
MCP["MCP 도구 셋<br/>(이 프로세스 환경에 정의된 것만)"]
end
subgraph USER_CC["사용자 Claude Code 본 세션 (별개 프로세스)"]
direction TB
USER_LLM["Claude CLI"]
USER_MCP["MCP 도구 셋<br/>(사용자 OAuth/토큰)"]
end
SL --> SH1
SL --> SH2
SL --> SH3
SL --> SH4
SH1 & SH2 & SH3 & SH4 --> ROUTER
ROUTER --> SESSION
SESSION --> DISPATCH
DISPATCH --> PROV_C & PROV_X & PROV_G
PROV_C --> LLM
PROV_X --> LLM
PROV_G --> LLM
LLM --> MCP
LLM -->|응답 텍스트| DISPATCH
DISPATCH -->|say + persona bot_token| SL
SL -->|페르소나 명의 게시| U
USER_LLM -.->|*완전히 분리*| USER_MCP
style USER_CC stroke:#f85149,stroke-dasharray: 5 5
style LLM_PROC stroke:#3fb950
style Pantheon stroke:#58a6ff
핵심: 왼쪽 Pantheon 영역과 오른쪽 사용자 Claude Code 영역은 같은 머신에서 돌지만 별개 프로세스다. MCP 도구 연결도 각 영역이 별도로 보유한다.
레이어 분해
레이어 1: 메시지 수신 (페르소나별 독립 봇)
확정 사실 (bridge.py:46-47, 134-159, 655, 3396-3483)
- Pantheon은 단일 Python 프로세스 안에 6개의 Slack Bolt App을 띄운다. 페르소나마다 별도
Bolt App+ 별도SocketModeHandler+ 별도bot_token(JINI_BOT_TOKEN,WANSU_BOT_TOKEN등). - 같은 Slack 메시지가 멘션·DM·채널 트리거 조건에 따라 여러 봇 App에 동시 도착할 수 있다. 그래서
_dup_guard(30분 TTL 파일) +_in_flightdict로 중복 응답을 막는다. - 이벤트 타입은 두 개:
app_mention(직접 멘션),message(스레드/채널 본문).
핵심 함의: 페르소나는 각자 자기 Slack 앱이다. 봇 토큰을 분리해야 발화 주체가 보존된다. 그래서 jini가 hangman 토큰으로 post하면 주체 혼동이 생긴다 ([feedback_no_persona_via_user_token]).
레이어 2: 라우팅 (어느 페르소나가 응답할지)
확정 사실 (bridge.py:382-433, 1893-1906, 1998-2004)
라우팅은 프리픽스 → 채널 자동응답 → northstar 게이트 3단계로 결정된다.
| 단계 | 트리거 | 동작 |
|---|---|---|
| 1. 강제 프리픽스 | !jini, !wansu 등 |
해당 페르소나가 무조건 응답 |
| 2. 자동응답 채널 | JINI_AUTO_CHANNELS, WANSU_AUTO_CHANNELS 환경변수 |
태그 없는 메시지도 응답 후보 |
| 3. Northstar gate | #northstar 또는 자동응답 채널 + 무태그 |
LLM이 응답할지 말지 스스로 판단 (NO_RESPONSE 키워드로 침묵 선택) |
추정: 같은 채널에 여러 페르소나가 자동응답 후보로 있으면 각자 northstar gate를 돌고 응답 가치가 있다고 판단한 페르소나만 실제 답한다. 이게 dispatcher 2세대(project_dispatcher_agent)에서 다듬는 중인 영역.
레이어 3: 세션·메모리 (대화 연속성)
확정 사실 (bridge.py:1973-2041, persona_sessions.py, persona_memory.py)
세션 영속화는 3겹 구조다.
flowchart LR
M["메시지 도착"] --> S1
S1["1. sessions.json<br/>{persona}:{thread_ts}<br/>= claude session_id"]
S2["2. persona_sessions<br/>채널별 상태<br/>(7d TTL)"]
S3["3. persona_memory<br/>episodic JSONL<br/>(검색 가능)"]
S1 -.->|--resume| LLM["LLM 프로세스"]
S2 -.->|컨텍스트 주입| LLM
S3 -.->|관련 회상| LLM
- 세션 ID 매핑: 같은 스레드의 후속 메시지는 같은 LLM 세션을
claude -p --resume <session_id>로 재개. 따라서 스레드 = 영속 대화 단위. - 채널별 상태: 채널 단위로 7일 TTL 상태 저장 (예:
#northstar에서의 jini 컨텍스트). - Episodic 메모리: 매 회차 발화를 JSONL로 누적 → 새 메시지마다 관련 과거 발화를 검색해 프롬프트에 주입. 이게 상단 시스템 reminder의 "Jini recent episodic memory" 출처.
핵심 함의: 세션 영속성은 Pantheon 측 파일 시스템이 책임진다. 페르소나가 "기억한다"는 것은 Pantheon이 다음 호출에 과거 텍스트를 다시 주입한다는 뜻이지, LLM 프로세스가 살아 있다는 뜻이 아니다. LLM 프로세스는 매 메시지마다 새로 spawn된다.
레이어 4: LLM 호출 + 도구 접근 (핵심)
확정 사실 (providers/claude.py:64-190, bridge.py:1675-1777)
sequenceDiagram
participant B as bridge.py
participant P as providers/claude.py
participant CLI as claude CLI (subprocess)
participant MCP as MCP 서버들
B->>P: ask_llm(persona, prompt, session_id)
P->>CLI: subprocess.Popen(["claude", "-p", prompt, "--resume", session_id])
Note over CLI: 새 프로세스. 환경변수·MCP config 상속
CLI->>MCP: tools 호출 (있는 것만)
MCP-->>CLI: 결과
CLI-->>P: stream JSON (type=result)
P-->>B: LLMResponse(text, session_id, usage)
확정 사실: 페르소나는 Claude CLI 서브프로세스로 매번 새로 뜬다 (providers/claude.py:64). MCP 도구 노출은 이 서브프로세스의 환경에 의해 결정된다.
추정: 페르소나가 보는 MCP 도구 목록은 Claude CLI가 인식하는 MCP config가 결정하는데, 그 config가 사용자 Claude Code 본 세션과 어디까지 공유되는지가 현재 보고서의 핵심 모호점. 두 가지 시나리오:
| 시나리오 | 결과 |
|---|---|
A. 페르소나도 ~/.claude/settings.json을 동일하게 읽음 |
본 세션에 연결된 MCP는 페르소나에도 보여야 함 |
| B. 페르소나는 별도 MCP config 사용 | 페르소나용 도구를 명시적으로 추가해야 함 |
콜로세움 케이스 관찰: Jini는 Google Drive MCP 도구는 보이는데 Notion MCP 도구(notion-search, notion-fetch)는 안 보이고 authenticate만 보임. 추정: Notion MCP는 OAuth 흐름이 페르소나 측에서 미완성 상태로 부팅된 것. Google Drive MCP는 동일 OAuth임에도 정상 노출되므로 MCP 별 인증 상태 차이가 본질.
레이어 5: 응답 게시 (페르소나 명의 보존)
확정 사실 (bridge.py:2250~, tools/slack_send.py:20-27)
- 메인 응답 경로: 각 Bolt App 인스턴스가
say(reply, thread_ts=...)호출 → 자기 봇 토큰으로 자기 명의로 게시. - 별도 트리거 (서브에이전트·CLI에서 페르소나 명의 발화 필요할 때):
tools/slack_send.py --persona jini→JINI_BOT_TOKEN환경변수로 직접 chat.postMessage 호출 ([reference_pantheon_slack_send]).
핵심 함의: 페르소나 명의는 봇 토큰 분리로 강제된다. 사용자 토큰으로 post하면 사용자 본인 명의로 보임 → 페르소나 정체성 무너짐.
MCP 도구 노출 매트릭스 (관측 기반)
콜로세움 케이스에서 jini 페르소나에 노출된 deferred 도구 목록을 관측 사실로 정리:
| MCP 서버 | 페르소나 측 노출 | 사용자 본 세션 노출 | 원인 추정 |
|---|---|---|---|
| Slack (slack-hangman) | ✅ 전체 | ✅ 전체 | bot/user 토큰 모두 환경에 박혀 있음 |
| Google Drive | ✅ 전체 | ✅ 전체 | OAuth 인증 완료 상태로 부팅 |
| Linear (hangman) | ✅ 전체 | ✅ 전체 | API 키 환경변수 박혀 있음 |
| GCal | ✅ 전체 | ✅ 전체 | OAuth + 토큰 파일 존재 |
| Notion | ⚠️ authenticate만 |
✅ 전체 | 페르소나 측 OAuth 토큰 미주입 |
| Chrome DevTools | (확인 필요) | ✅ | — |
핵심 패턴: Bearer/API 키 기반 MCP는 페르소나도 정상, OAuth 기반인데 토큰 파일이 페르소나에 안 보이면 실패. Google Drive는 같은 OAuth지만 토큰이 공유되는 위치에 있는 것으로 추정.
도구 관리 전략 옵션 3가지
이 구조 이해를 바탕으로 앞으로 도구를 어떻게 관리할지 3가지 옵션을 펼친다.
옵션 A: 페르소나 MCP를 사용자 본 세션과 완전히 공유
페르소나가 spawn될 때 사용자 ~/.claude/settings.json (또는 동등 위치)을 그대로 상속하게 만든다.
| 장점 | 단점 |
|---|---|
| 도구 추가 시 한 곳만 관리 | 페르소나가 사용자 본인 명의 API를 호출 → 발화/액션 주체 혼동 위험 (예: Notion에 jini가 항승 명의로 페이지 생성) |
| 사용자가 본 세션에서 OAuth 한 번 하면 페르소나도 즉시 사용 | 보안 사고 시 blast radius 큼 |
옵션 B: 페르소나별 전용 도구 셋 (현재 구조의 의도된 형태로 추정)
페르소나마다 필요한 MCP만 명시적으로 등록. OAuth도 페르소나 명의의 별도 인증 (예: Notion에 jini integration을 별도 생성).
| 장점 | 단점 |
|---|---|
| 발화·액션 주체 명확. Slack 봇 토큰 분리와 같은 철학 | 도구 추가마다 N개 페르소나에 각각 셋업 |
| 권한 최소화 (raphael은 read-only만 등) | 운영 비용·복잡도 증가 |
옵션 C: 공유 도구 + 명의 분리 (하이브리드)
읽기 도구는 사용자 토큰 공유 (저비용), 쓰기 도구는 페르소나 명의 강제 (안전).
| 장점 | 단점 |
|---|---|
| 검색·조회는 빠르게 추가 가능 | 도구별로 읽기/쓰기 라우팅 룰을 따로 관리해야 함 |
| 사고 위험은 쓰기 경로에 집중 | MCP 서버가 read/write를 도구 레벨에서 분리해줘야 깔끔 |
Risks (운영 관점)
| 위험 | 영향 | 대응 후보 |
|---|---|---|
| 페르소나 환경 부팅 시 어떤 토큰이 들어가는지 문서화되지 않음 | 신규 MCP 추가 시 디버깅 비용 증가 | tools/ 아래에 페르소나별 env 점검 스크립트 추가 |
| 페르소나 LLM 프로세스가 매번 새로 뜸 → MCP OAuth handshake 매번 필요한 경우 지연·실패 | 응답 latency, 간헐적 도구 누락 | OAuth 토큰은 파일 영속화 + 환경변수 주입 표준화 |
| 페르소나가 사용자 토큰으로 외부 API 호출 → 발화 주체 혼동 | 신뢰·감사 추적 어려움 | 옵션 B/C 채택 시 자동으로 해소 |
결정 위임 — 항승씨에게 묻고 싶은 것
전략 옵션을 좁히려면 사용자 가치 판단이 필요해요. 본 보고서에서 세 가지를 묻고 싶어요.
- 옵션 A vs B vs C 중 어느 철학에 가까운가? 직관적으로 끌리는 쪽이 있는지, 아니면 케이스별로 갈 생각인지.
- 페르소나가 "사용자 본인 명의로" 외부 시스템 액션을 하는 게 어디까지 허용되는가? (Notion 페이지 생성, Linear 이슈 close 등.) 지금까지의 룰은 Slack post는 절대 금지인데 다른 시스템은 명시되지 않음.
- Notion 케이스는 이번에 풀고 끝낼 것인가, 일반 패턴으로 풀 것인가? 지금 페르소나 측 Notion만 따로 고치면 Quick fix이고, OAuth MCP 일반 패턴으로 풀면 같은 사고 재발 방지에 더 가치.
모름·확인 필요
- 페르소나 LLM 프로세스가 어떤 MCP config 파일을 어떤 우선순위로 읽는지 (claude CLI 내부 동작) → 사용자 본 세션에서
claude --print-config류 확인 필요 - Google Drive MCP는 페르소나에서 정상 노출, Notion만 실패한 구체적 토큰 파일 위치 차이 →
~/.config/claude/또는 OS keychain 비교 필요 - Chrome DevTools MCP가 페르소나에 노출되는지 (콜로세움 케이스에서 미확인)
링크·참조
- 소스 인용:
pantheon/bridge.py,pantheon/providers/claude.py,pantheon/persona_sessions.py,pantheon/persona_memory.py,pantheon/tools/slack_send.py - 관련 메모리:
reference_pantheon_slack_send,project_dispatcher_agent,feedback_pantheon_persona_skill_execution,feedback_no_persona_via_user_token,reference_slack_mention_format - 관측 케이스: 콜로세움 트래커 row fetch (2026-06-08 #northstar 스레드)