마지막 업데이트 2026-09-12
RPC response received before ack 문구는 PPI가 만든 오류가 아니라 LiveKit JavaScript SDK의 RpcClientManager가 출력한 경고다.
PPI의 세션 로그 수집기가 SDK의 console.warn을 ctx: "CONSOLE"로 저장했다.
로그에서 이 경고가 약 2초 간격으로 반복된 직접 이유는 PPI가 Runtime RPC에
responseTimeout: 2000을 전달하기 때문이다. LiveKit SDK는 ACK를 최대 7초 기다리지만,
로컬 응답 Promise는 2초에 먼저 거절될 수 있다. Promise의 finally가 실행될 때 ACK가 아직
pending이면 SDK가 위 경고를 남기고 ACK 타이머를 제거한다.
RPC Response 패킷이 ACK보다 먼저 도착했다는 증거는 없다.
또한 요청이 Agent에 도달하지 않았는지, ACK가 지연됐는지, Agent 응답이 늦었는지는 브라우저 로그만으로 확정할 수 없다.
분석 대상은 lesson-log-20260827.jsonl.gz 한 건이다. 파일명과 달리 실제 형식은 gzip이 아닌 평문 JSONL이며 정상적으로 읽을 수 있었다.
PPI input policy / runtime control
→ apps/web/lib/voice-agent/livekit-client-session.ts
→ room.localParticipant.performRpc({ responseTimeout: 2000 })
→ livekit-client 2.19.2 / RpcClientManager.performRpc()
→ LiveKit reliable data transport
→ remote Agent RPC handler
→ ACK / response
SDK console.warn(...)
→ apps/web/lib/lesson-log-sink.ts
→ ctx: "CONSOLE" 세션 로그
| 구간 | 확인한 위치 | 역할 |
|---|---|---|
| PPI RPC 호출 | apps/web/lib/voice-agent/livekit-client-session.ts:932-942 | 모든 Runtime RPC에 2초 응답 제한 적용 |
| 입력 상태 RPC | apps/web/lib/voice-agent/livekit-client-session.ts:1048-1067 | ppi.set_input_enabled payload 전송 |
| 콘솔 수집 | apps/web/lib/lesson-log-sink.ts:29-47 | 외부 라이브러리의 warn/error를 lesson log로 저장 |
| SDK 버전 | apps/web/package.json:51 | livekit-client 2.19.2 선언 |
| 세션 | 건수 | 인접 간격 |
|---|---|---|
| 세션 A | 10 | 2013, 2006, 2004, 2010, 23979, 2006, 2008, 2007, 2016ms |
| 세션 B | 7 | 2011, 2006, 2395, 2008, 2013, 2006ms |
Response timeout을 포함한 상위 오류 로그는 50건이었다. 하나의 실패가 여러 계층에서 기록되므로 고유 RPC 수와 동일한 값은 아니다.2004~2016ms에 모였다.2395ms와 23979ms 공백도 있어 전체 17건이 끊김 없이 정확히 2초 주기인 것은 아니다.| 값 | 적용 주체 | 의미 |
|---|---|---|
| 2,000ms | PPI 호출자 | 브라우저가 응답 Promise를 기다리는 로컬 제한 |
| 7,000ms | LiveKit SDK | 요청 도달과 ACK 왕복에 허용하는 최대 시간 |
| 8,000ms | LiveKit SDK → 상대 | maxRoundTripLatencyMs + 1000으로 계산된 최소 유효 처리 제한 |
responseTimeoutMs와 상대에게 전달하는 effectiveTimeoutMs를 별도로 관리한다.
공식 v2.19.2 테스트도 50ms 응답 타임아웃이 7초 ACK 타임아웃보다 먼저 끝나는 경우를 명시적으로 검증한다.
따라서 PPI의 2초 설정은 API 규약상 유효하다. 다만 상대에는 최소 8초가 전달되는데 호출자는 2초 만에 포기하므로, 이 사건처럼 원인 분류가 흐려지고 늦게 도착한 ACK/응답이 이미 정리된 요청으로 취급될 가능성이 생긴다.
pendingAcks와 pendingResponses를 등록하고
ACK 7초 타이머를 시작한다. 상대에게 전달되는 처리 제한은 최소 8초다.
responseTimeout이 먼저 만료되어 RESPONSE_TIMEOUT으로 Promise가 거절된다.
completionFuture.promise.finally()가 실행된다. ACK가 여전히 pending이면
RPC response received before ack를 출력하고 ACK 상태와 7초 타이머를 제거한다.
CONNECTION_TIMEOUT 경로는 실행되지 않는다.
finally가 실행된다. 따라서 RESPONSE_TIMEOUT에서도 같은 경고가 나올 수 있다.
| 가설 | 판정 | 근거 |
|---|---|---|
| PPI 코드가 해당 경고를 직접 생성했다 | 기각 | 문구는 LiveKit SDK v2.19.2의 RpcClientManager에 존재하며 PPI에서는 일치 문자열이 없다. |
| 2초 로컬 타임아웃이 ACK pending 경고를 촉발했다 | 유력 | 17건 중 대부분이 약 2초 간격이고, 같은 시각대에 Response timeout 상위 오류가 동반된다. |
| 네트워크에서 Response 패킷이 ACK보다 먼저 도착했다 | 미확정 | 브라우저 로그에 요청별 ACK·응답 수신 시각이 없고, 경고가 실제 response 수신 없이도 발생한다. |
| Agent 또는 전송 구간이 ACK/응답을 누락·지연했다 | 미확정 | 동일 request ID를 가진 Agent 수신·처리 로그가 제공되지 않았다. |
| 현재 동작 | 목표 동작 |
|---|---|
| 모든 Runtime RPC가 2초 제한을 공유하고, 2초 종료 시 SDK 경고가 실제 패킷 역전처럼 보인다. | RPC 종류별 실제 지연 분포에 맞는 제한을 사용하고 연결 실패와 응답 지연을 구분한다. |
| 브라우저 로그만으로 요청 도달·ACK·Handler 실행·응답 경계를 나눌 수 없다. | request ID 기준으로 브라우저와 Agent의 전 구간 타임스탬프를 연결한다. |
| 입력 ON/OFF처럼 연속되는 상태 변경 RPC가 늦게 처리될 때 최종 상태 일관성을 확인할 수 없다. | 지연·중복·늦은 응답에서도 마지막 desired state와 Agent 상태가 일치함을 검증한다. |
file lesson-log-20260827.jsonl.gz
jq -s '[.[] | select(.msg=="RPC response received before ack")] | length' "$LOG"
jq -s '... group_by([.sessionId,.instanceId]) ... 인접 ts 차이 ...' "$LOG"
jq -s '[.[] | select(.msg=="RPC response received before ack") | .data]
| {count:length, unique_data_count:(unique|length)}' "$LOG"
performRpc 호출과 2초 설정을 현재 저장소 소스로 확인했다.ctx: "CONSOLE"로 저장하는 것을 확인했다.client-sdk-js v2.19.2 태그에서 7초 ACK 타이머, 8초 최소 유효 처리 제한, 경고 발생 조건을 확인했다.timeout을 늘리는 것만으로 ACK 누락이나 Agent 처리 실패의 근본 원인이 해결되지는 않는다.
node_modules 산출물을 직접 검사하지 않았다.apps/web/lib/voice-agent/livekit-client-session.tsapps/web/lib/lesson-log-sink.tslesson-log-20260827.jsonl.gz