iop/apps/node
toki 9ef4418f8f fix(node): suppress previous assistant messages in Claude TUI output filter
Add baselineAssistant tracking to prevent repainted/stale assistant
messages from being emitted when a new prompt is being entered.
Introduce latestClaudeAssistantMessageAfterPromptFromCleanOutput to
detect assistant messages that appear after the current prompt echo,
and isClaudePromptEchoLine to recognize prompt echo lines. Add tests
for suppression of previous replies while prompt echoes and for
allowing same text after current prompt.
2026-06-01 21:50:13 +09:00
..
cmd/node refactor: migrate packages to packages/go/ structure 2026-06-01 10:03:55 +09:00
internal fix(node): suppress previous assistant messages in Claude TUI output filter 2026-06-01 21:50:13 +09:00
README.md refactor: migrate packages to packages/go/ structure 2026-06-01 10:03:55 +09:00

node — Node Agent

디바이스당 1개 실행되는 IOP 노드 에이전트.
Edge에 연결되어 adapter + target 실행을 수행하고, TCP/protobuf 소켓을 통해 요청, 명령, 이벤트 스트림을 처리한다. mTLS helper는 packages/go/auth에 있지만 현재 Edge-Node transport에는 아직 연결하지 않았다.

내부 구조 (Hexagonal Architecture)

cmd/node/            — CLI 진입점 (cobra)
internal/
  bootstrap/         — fx 의존성 주입 모듈
  node/              — 핵심 노드 서비스 (transport.Handler 구현)
  runtime/           — 도메인 타입: ExecutionSpec, RuntimeEvent, 인터페이스
  router/            — RunRequest → ExecutionSpec 라우팅
  transport/         — Edge로 연결하는 TCP/protobuf client와 session 처리
  adapters/
    mock/            — 에코 테스트 어댑터
    ollama/          — Ollama /api/chat 스트리밍 어댑터
    vllm/            — vLLM OpenAI-compatible 어댑터 skeleton
    cli/             — CLI 프로세스 어댑터 (claude/antigravity/codex/opencode/cline)
  store/             — SQLite 실행 이력

실행 경계

field 사용자 기본 경로에서 Node는 별도 설정 파일을 만들거나 편집하지 않는다. Edge가 제시한 bootstrap 명령 한 줄을 실행하면 Node binary 다운로드, 검증, Edge 연결, foreground 실행까지 이어져야 한다.

아래 명령은 Node binary 자체의 저수준 개발/진단용 표면이다. 사용자-facing field bootstrap 안내로 쓰지 않는다.

# 빌드
make build-local

# 버전 확인
./build/bin/iop-node version

# 저수준 직접 실행은 내부 진단에서만 사용한다.

호스트 환경 준비 참고

setup은 현재 구현된 host setup 보조 기능이다. field 사용자 기본 경로는 Node setup/config가 아니라 Edge가 제시한 bootstrap 명령이다. 새 사용자-facing 흐름을 설계할 때 이 표면을 기본 경로로 확장하지 않는다.

현재 구현된 node CLI 표면은 다음과 같다.

iop-node serve
iop-node version

원격 edge에 붙는 사용자/field 테스트는 Edge가 제시한 bootstrap 명령을 실행한다. repo root의 scripts/dev/node.sh는 과거 개발 진단 helper로만 취급하고, 문서나 runbook의 공식 사용자 경로로 안내하지 않는다.

edge 예시 설정은 cli adapter의 opencode profile을 사용한다. configs/edge.yamlconsole.targetclaude, claude-tui, antigravity, codex, opencode, cline-dgx 같은 profile 이름으로 바꾸면 같은 실행 파이프라인에서 다른 CLI target을 검증할 수 있다.

edge에서 실행 요청을 받으면 node는 해당 입력을 선택된 adapter target으로 전달하고, adapter가 emit한 delta/event를 Edge로 스트리밍한다.

[edge-message] hello
[node-event] start run_id=manual-...
[node-event] complete run_id=manual-... detail="opencode sse execution complete"
[node-message] <adapter output>

Edge-Node 메시지 경계

node는 같은 TCP/protobuf session 위에서 여러 메시지 계열을 처리한다.

  • RunRequest: adapter execution 시작. CLI adapter에서는 prompt 전달로 해석된다.
  • CancelRequest: 실행 취소 또는 logical session 종료.
  • NodeCommandRequest: 실행이 아닌 node/adapter 조회성 명령. 현재 USAGE_STATUS, CAPABILITIES, SESSION_LIST, TRANSPORT_STATUS를 지원한다.
  • RunEvent: adapter execution stream. start, delta, complete, error, cancelled 같은 실행 이벤트를 edge로 보낸다.
  • EdgeNodeEvent: edge-node lifecycle/control event envelope. edge 연결 해제 등 실행 스트림이 아닌 이벤트를 다룬다.

즉 CLI에 메시지를 보내는 요청 파이프라인과 이벤트 파이프라인은 논리적으로 분리되어 있고, 물리 transport만 공유한다.

Local Execution History

Node local SQLite store는 node가 직접 수행한 run의 최소 상태(run_id, adapter, target, session_id, background, status, timestamps, error)를 보관한다. 이 DB는 현재 node-local 복구/진단용 실행 이력이며 Edge event aggregation이나 Control Plane 전역 이력 저장소가 아니다. Edge와 상위 운영면은 실행 stream/event를 Edge 경유로 관찰하고, Node DB를 직접 읽는 구조를 전제로 하지 않는다.

Logical Session (transport 1개 · session 여러 개)

edge-node transport 연결은 호스트당 1개를 유지한다. 그 연결 위에서 CLI adapter는 session_id가 다른 여러 장수 worker process를 독립적으로 관리한다.

개념 설명
transport 연결 edge-node 호스트 쌍당 1개 TCP 연결
logical session (adapter, target, session_id) 로 식별되는 장수 worker process
run session 위에서 실행되는 단일 요청

cancel vs terminate:

  • CancelAction_CANCEL_RUN (기본값): 현재 실행 중인 run만 중단. session process는 살아있다.
  • CancelAction_TERMINATE_SESSION: session process를 명시적으로 종료. 이후 같은 session_id로의 요청은 새 process를 만들거나(CREATE_IF_MISSING) 에러를 반환한다(REQUIRE_EXISTING).

session_mode:

  • RUN_SESSION_MODE_CREATE_IF_MISSING (기본값): session이 없으면 새로 생성.
  • RUN_SESSION_MODE_REQUIRE_EXISTING: session이 없으면 에러 반환. 새 process 생성 금지.

Node Commands

node는 adapter execution(RunRequest) 외에도 edge가 보내는 NodeCommandRequest를 처리한다. 현재 구현된 command:

  • USAGE_STATUS: 선택된 adapter/target (예: cli/codex)의 사용량 한도와 초기화 시간을 조회한다.
    • Codex의 경우 TUI를 시작하고 /status 명령을 전송한 뒤 출력된 % left 정보를 파싱해 AgentUsageStatus로 반환한다.
    • 그 외 adapter/target은 지원하지 않는 경우 명시적 에러를 반환한다.
  • CAPABILITIES: 요청한 adapter의 capability를 조회한다. 현재 응답은 adapter 이름, target 목록, max_concurrency를 포함한다.
  • SESSION_LIST: CLI adapter가 관리하는 logical session 목록을 조회한다. 응답 result map에 count, sessions(쉼표 구분 mode:target/session_id 레이블) 외에 session.N.mode, session.N.target, session.N.session_id structured key가 포함된다. edge console은 이 key를 감지해 session별 grouped output으로 표시한다.
  • TRANSPORT_STATUS: node 관점의 edge 연결 여부와 요청 echo fields(node_id, adapter, target, session_id)를 조회한다. heartbeat 상세 카운터는 현재 응답에 포함되지 않는다.

어댑터

어댑터 설명 상태
mock 입력 에코, 스트리밍 테스트용 구현 완료
ollama 로컬 Ollama /api/chat 스트리밍 연동 기본 구현 완료
vllm vLLM OpenAI-compatible API /v1/models 조회 skeleton
cli claude/antigravity/codex/opencode/cline CLI 실행 구현 완료

Ollama adapter는 edge에서 받은 내부 target을 Ollama model 이름으로 사용한다. Edge OpenAI-compatible HTTP API에서 openai.target을 지정하면 외부 model 값과 무관하게 해당 Ollama model로 라우팅하고, openai.target을 비우면 요청의 model 값을 그대로 내부 target으로 전달한다. adapters.ollama.context_size가 설정되어 있으면 Ollama /api/chat 요청의 options.num_ctx로 전달한다.

claude, antigravity, codex, opencode, cline처럼 기본이 interactive TUI인 CLI는 기본적으로 non-interactive 모드로 설정한다. 예: claude -p --dangerously-skip-permissions, agy --dangerously-skip-permissions --print <prompt>, codex exec --dangerously-bypass-approvals-and-sandbox, cline -y --json --config /config/.cline/profiles/ollama-dgx <prompt>.

Claude는 용도별로 두 profile을 둔다. claudeclaude -p 기반 one-shot profile이며 Agent SDK/credit 경로 검증용으로 유지한다. claude-tuipersistent: true, terminal: true, mode: "persistent-lazy"로 일반 Claude Code TUI를 첫 요청 시 lazy start한다. TUI 출력은 structured JSON이 아니므로 idle timeout 기반으로 completion을 판단한다.

Antigravity는 mode: "antigravity-print"로 설정해 TUI가 아닌 print mode에서도 IOP session_id별 conversation을 이어간다. 첫 실행은 agy --print <prompt>로 새 conversation을 만들고 로그에서 conversation id를 읽어 저장한다. 같은 IOP session의 다음 실행은 resume_args를 사용해 agy --conversation <conversation_id> --print <prompt> 형태로 재개한다.

opencode는 profile mode: "opencode-sse"로 설정해 opencode serve의 HTTP/SSE 인터페이스를 사용한다. profile.Command가 가리키는 opencode 바이너리를 serve --hostname 127.0.0.1 --port 0으로 띄우고 /global/event SSE에서 text delta를 RuntimeEvent delta로 relay한다. 외부에서 이미 실행 중인 server에 연결하려면 --attach <url>을 args에 추가한다. legacy output_format: "opencode-json" stdout JSONL 경로는 opencode run --format json 사용 시에만 의미가 있다.

Cline은 mutable --config 디렉터리를 프로필처럼 나눈다. /config/.cline/profiles/ollama-dgx/config/.cline/profiles/ollama-m1opencodeollama-dgx/ollama-m1 provider 설정을 Cline의 ollama provider 설정으로 변환한 값이다.

Transport

  • TCP 4-byte length-prefix framing
  • protobuf message framing via common-proto-socket
  • edge 연결 해제는 EdgeNodeEvent로 로컬 표시되며, 실행 스트림 RunEvent와 분리한다.
  • mTLS helper는 존재하지만 현재 transport 설정에는 아직 연결되지 않음
  • 하트비트: 30초 간격