OpenClaw 자동화

답장 기다리는 중에 메시지를 또 보내면|OpenClaw 큐 모드 steer·followup·collect·interrupt

깊음위에 2026. 9. 28. 08:25

AI 에이전트한테 일을 시켜놓고 기다리는 동안, 생각이 하나 더 떠오릅니다. "아, 그거 할 때 A 말고 B로 해줘." 그래서 메시지를 하나 더 보냅니다. 여기서 사람마다 기대가 갈립니다. 지금 하는 일을 멈추고 새 지시를 따를 거라고 생각하는 사람도 있고, 지금 일이 끝난 뒤에 처리될 거라고 보는 사람도 있습니다.

둘 다 가능한 동작이고, OpenClaw에서는 이걸 큐 모드(queue mode)로 고릅니다. 기본값은 "가능하면 지금 돌고 있는 작업 안으로 밀어 넣는다"입니다. 이 글은 OpenClaw 공식 문서의 concepts/queue, concepts/queue-steering, tools/steer 세 문서를 한국어 독자 기준으로 풀어 정리한 해설입니다. 출처는 docs.openclaw.ai이며, 제가 특정 환경에서 측정한 지연 시간이나 성능 수치는 들어 있지 않습니다.

왜 줄을 세우는가

먼저 전제입니다. OpenClaw는 들어오는 자동응답 실행을 작은 인프로세스 큐로 직렬화합니다. 텔레그램·슬랙·디스코드·왓츠앱·웹챗 같은 채널이 전부 이 파이프라인을 씁니다. 이유는 문서에 두 가지로 적혀 있습니다.

  • 자동응답 실행은 LLM 호출이라 비싸고, 메시지가 몰리면 서로 충돌합니다.
  • 직렬화하면 세션 상태·로그·CLI 표준입력 같은 공유 자원 경쟁을 피하고, 상위 API의 레이트 리밋에 걸릴 확률도 줍니다.

다만 "전부 한 줄로 세운다"는 뜻은 아닙니다. 레인(lane)별 FIFO 큐이고 레인마다 동시 실행 상한이 따로 있습니다. 설정되지 않은 레인은 기본 1, main 레인은 max(8, 사용 가능한 CPU 병렬도 × 4), subagent 레인은 기본 8입니다. 핵심 보장은 세션 하나에는 동시에 한 번의 실행만 닿는다는 것입니다.

기본값 네 줄

아무것도 설정하지 않았을 때 모든 인바운드 채널의 기본값입니다. 이것만 알아도 체감 동작이 설명됩니다.

  • mode: "steer" — 같은 턴 안으로 밀어 넣기
  • steer·followup·collect 배칭에 내장 500ms 디바운스
  • cap: 20 — 세션당 최대 대기 메시지 수
  • drop: "summarize" — 넘치면 오래된 것부터 버리되 요약은 남김

모드 네 가지

/queue 명령으로 고릅니다. 이미 실행 중인 세션에 평소 메시지가 들어왔을 때의 동작이 달라집니다.

모드 실행 중일 때 그 뒤
steer(기본) 가능하면 활성 런타임에 지시를 주입 주입이 불가능하면 현재 실행이 끝날 때까지 기다린 뒤 시작
followup 주입하지 않음 현재 실행이 끝난 뒤 별도 턴으로 각각 실행
collect 주입하지 않음 조용해질 때까지 모아서 하나의 후속 턴으로 합침
interrupt 현재 실행을 중단 가장 최신 메시지로 새로 시작

collect에는 단서가 하나 붙습니다. 모인 메시지가 서로 다른 채널이나 스레드를 향하고 있으면 합치지 않고 따로 흘려보냅니다. 라우팅을 보존하려는 처리입니다.

steer는 도구를 중간에 끊지 않는다

여기가 이 주제에서 가장 많이 오해받는 부분입니다. 문서는 명확합니다. 스티어링은 이미 실행 중인 도구 호출을 중단시키지 않습니다. 대신 런타임이 정해진 경계에서만 확인합니다. 순서를 그대로 옮기면 이렇습니다.

  1. 어시스턴트가 도구 호출을 요청합니다.
  2. 순차 모드에서는 각 호출이 시작되기 직전에 확인합니다. 비동기 해석·검증·사전 실행 훅이 끝난 뒤도 포함입니다.
  3. 돌고 있던 호출은 끝까지 갑니다. 그 뒤에 스티어가 대기 중이면, 아직 시작하지 않은 순차 호출들의 나머지는 건너뜁니다.
  4. 병렬 모드에서는 호출을 먼저 준비하고, 준비된 것들을 띄우기 직전에 한 번만 확인합니다. 그 체크포인트를 넘은 호출들은 같이 끝까지 갑니다.
  5. 건너뛴 호출에도 짝이 되는 시작·종료 이벤트와 합성 결과(Skipped to process an incoming message.)가 어시스턴트 원래 순서대로 들어갑니다. Control UI에는 Skipped로 표시됩니다.
  6. 그리고 다음 LLM 호출 전에, 실제로 빠져나온 스티어링 메시지가 그대로 붙습니다.

합성 결과를 채워 넣는 이유가 중요합니다. 모든 도구 호출이 결과와 짝을 유지해야 기록이 구조적으로 유효하고, 모델은 "그 도구가 실행되지 않았다"는 사실을 알게 됩니다. 기록은 계속 append-only로 남습니다.

부수 효과로 알아둘 것도 있습니다. 서브에이전트 완료 보고 같은 내부 업데이트도 같은 스티어링 경계를 씁니다. 이 업데이트는 대화 기록에서 숨겨질 수 있고 사용자 메시지 큐에 나타나지 않습니다. 그래서 도구가 Skipped로 떴다고 해서 반드시 사람이 보낸 메시지가 대기 중이라는 뜻은 아닙니다. 서브에이전트 쪽 구조가 궁금하시면 OpenClaw 서브에이전트 정리 글을 같이 보시면 연결이 됩니다.

멈추는 것과 방향을 바꾸는 것은 다른 의도

문서가 한 문장으로 정리해 둔 대목입니다. 이미 돌고 있는 일을 멈추는 것과 앞으로 할 일을 돌리는 것은 다른 의도입니다. 새 메시지가 현재 실행을 없애야 하는 상황이면 /queue interrupt(또는 /stop)를 쓰는 게 맞습니다. steer로는 안 됩니다.

답장이 여러 개로 쪼개져 보이는 이유

채널 스트리밍이 partial이나 block으로 켜져 있으면, 스티어링이 짧은 답장 여러 개처럼 보일 수 있습니다.

  • partial: 미리보기가 먼저 확정되고, 스티어링이 받아들여진 뒤 새 미리보기가 시작됩니다.
  • block: 초안 크기 블록들이 같은 순차적 인상을 만듭니다.
  • 스트리밍이 없고 런타임이 같은 턴 스티어링을 못 받으면, steer는 현재 실행 뒤의 followup으로 떨어집니다.

즉 답장이 여러 개로 보이는 것 자체는 고장이 아니라 표시 방식일 수 있습니다. 채널별 동작 차이가 궁금하시면 채널 선택·라우팅 정리 글이 배경으로 도움이 됩니다.

에이전트가 뭘 물어본 상태라면

이건 별도 경로입니다. 에이전트가 질문을 띄워둔 상태에서 평문으로 답하면, 그 답은 일반 큐 처리보다 먼저 그 질문으로 갑니다. 네이티브 CLI가 스티어링을 못 받는 경우에도 그렇습니다.

권한 판정 기준도 적혀 있습니다. OpenClaw는 답변을 질문을 만든 쪽의 권한과 활성 실행에 비춰 확인합니다. 다음 턴에 고른 모델 기준이 아닙니다. 권한이 바뀌었거나 질문을 만든 쪽이 닫혀 있으면, 새 턴을 시작하는 대신 명시적으로 거절합니다.

그리고 문서에 조심스러운 조항이 하나 더 있습니다. 답변이 반영됐을 수도 있는데 확인이 유실된 경우, OpenClaw는 그 불확실성을 보고하고 스티어링이나 후속 턴으로 다시 보내지 않습니다. 재시도 전에 대화를 먼저 확인하라는 안내가 붙어 있습니다. 나중에 전달이 실패하거나 원본 정리가 실패해도 그 답변이 재생 가능해지는 것은 아니고, 불확실성만으로 원래 실행이 취소되지도 않습니다.

큐 옵션과 우선순위

옵션은 대기 전달에 적용됩니다. debounce는 숫자만 쓰면 밀리초이고 ms·s·m·h·d 단위를 받습니다.

  • debounce: 대기 중인 followup·collect 배치를 흘려보내기 전의 조용한 창. Codex의 steer 모드에서는 배치된 turn/steer를 보내기 전 조용한 창 역할도 합니다.
  • cap: 세션당 최대 대기 메시지. 1보다 작은 값은 무시됩니다.
  • drop: "summarize"(기본): 필요한 만큼 오래된 것부터 버리되 짧은 요약을 남겨 합성 후속 프롬프트로 넣습니다.
  • drop: "old": 요약을 남기지 않고 오래된 것부터 버립니다.
  • drop: "new": 큐가 이미 꽉 찼으면 가장 새 메시지를 거부합니다.

설정은 messages.queue로 전역 또는 채널별로 둘 수 있습니다. 문서 예시에는 디스코드만 collect로, 디바운스는 1000ms로 두는 형태가 나옵니다.

모드 결정 순서는 이렇습니다. ① 인라인 또는 저장된 세션별 /queue 재정의 → ② messages.queue.byChannel.<channel> → ③ messages.queue.mode → ④ 기본값 steer. 옵션은 세션 쪽이 설정을 이기고, 그다음 채널별 디바운스, 플러그인 디바운스 기본값, 내장 기본값 순으로 적용됩니다. 한 가지 헷갈리기 쉬운 점은 cap과 drop은 전역·세션 옵션이고 채널별 설정 키가 아니라는 것입니다.

세션별로 바꿀 때는 /queue collect debounce:0.5s cap:25 drop:summarize처럼 묶어 쓸 수 있고, /queue default 또는 /queue reset으로 세션 재정의를 지웁니다.

/queue steer와 /steer는 다른 물건

이름이 비슷해서 자주 섞입니다. 문서 기준으로 나누면 이렇습니다.

  • /queue steer: 앞으로 평소 메시지들이 활성 실행을 스티어하게 만드는 설정.
  • /steer <메시지>: 저장된 /queue 설정과 무관하게, 지금 이 메시지를 활성 실행에 주입하려는 명시적 명령. /tell은 같은 명령의 별칭입니다.

/steer의 안전한 성질이 하나 있습니다. 현재 런타임이 스티어링을 못 받으면 메시지를 버리지 않고, 명령 접두사만 떼고 평범한 프롬프트로 이어서 보냅니다. 세션이 유휴 상태여도 같은 메시지로 정상 턴이 시작됩니다. 대상은 현재 세션의 활성 실행 하나뿐이고, ACP 하네스 세션을 노릴 때는 /acp steer를 씁니다.

실행 환경에 따른 제약도 있습니다. /steer·/tell은 게이트웨이를 거치는 명령이라 openclaw chat이나 openclaw tui --local에서는 명령으로 쓰지 않습니다. 그 경우 /queue steer를 고르고 지시를 평범한 메시지로 보내면 임베디드 런타임이 같은 정책을 적용합니다.

런타임에 따라 다르게 구현된다

스티어링은 런타임이 제공하는 기능이라 구현이 갈립니다. 네이티브 Codex 앱서버 하네스는 OpenClaw 내부 스티어링 큐 대신 turn/steer를 노출합니다. OpenClaw는 설정된 조용한 창 동안 대기 프롬프트를 모아 도착 순서대로 한 번의 turn/steer 요청으로 보냅니다. 그 뒤 도구 스케줄링은 Codex 쪽 스케줄러가 소유하고, 받아들인 스티어링을 다음 모델 경계에서 소비합니다. 이 런타임에는 OpenClaw가 도구별 선점을 덧붙이지 않습니다.

거절되는 경우도 명시돼 있습니다. Codex 리뷰 턴과 수동 컴팩션 턴은 같은 턴 스티어링을 받지 않습니다. 이때 steer 모드는 현재 실행이 끝날 때까지 기다린 뒤 프롬프트를 시작합니다. 컴팩션 자체가 뭘 하는지는 컴팩션과 세션 프루닝 정리 글에 따로 적어뒀습니다.

대기 중인 메시지를 취소할 수 있나

둘 다 가능한데 조건이 다릅니다.

후속·수집 큐에 앉아 있는 프롬프트는, 게이트웨이가 그 클라이언트 runId에 대해 취소 신원을 들고 있습니다. 권한이 있으면 chat.abort에 그 runId를 줘서 대기 중인 턴만 취소할 수 있습니다. runId 없이 세션에 대해 부르면 권한 있는 대기 턴을 먼저 취소하고 그다음 활성 실행을 중단합니다. 큐가 빠져나가면서 반쯤 멈춘 세션으로 작업이 올라가는 일을 막으려는 순서입니다. 다중 소유자 세션에서 요청자별 확인 없이 큐 전체를 비우는 것은 정지 경로가 아니라고 못 박아 뒀습니다.

런타임 스티어링 큐에서 아직 전달이 시작되지 않은 메시지도 chat.abort({ sessionKey, runId })로 철회할 수 있습니다. 활성 실행을 멈추거나 followup으로 재시도하지 않고 그 메시지만 빼는 동작입니다. 다만 전달이 시작된 뒤에는 철회나 완료된 작업의 되돌림을 보장하지 않습니다. 전달을 확인할 수 없으면, 소비 여부가 불확실한 입력을 재생하지 않기 위해 기존 안전장치가 활성 실행을 멈출 수 있습니다.

보였다고 소비된 것은 아니다

이 문장은 그대로 옮길 가치가 있습니다. 메시지가 화면에 보이거나 전송 확인이 떴다고 해서 활성 런타임이 그것을 소비했다는 뜻은 아닙니다. Control UI는 받아들인 메시지가 워커 준비나 작업공간 동기화를 기다리는 중일 때 그에 맞는 알림을 따로 보여줍니다.

입력 보존 범위도 구분해서 적혀 있습니다. 기존 세션으로 chat.send를 통해 들어온 일반 사용자 입력은 게이트웨이가 확인을 주기 전에 에이전트별 DB에 저장됩니다. Control UI·TUI·CLI·네이티브 앱·RPC 클라이언트가 모두 포함됩니다. 그래서 다른 클라이언트가 대기 중인 입력을 표시할 수 있습니다. 그런데 이건 입력 보존이고 실행 권한 보존이 아닙니다. 대기 입력이 기록에 닿기 전에 게이트웨이가 멈추면, 재시작 뒤에는 중단된 입력으로 보이고 명시적 재전송이 필요합니다. 메모리 큐는 재생되지 않습니다. 반대로 프로세스가 살아 있는 호스트 절전은 기존 큐를 그대로 이어갑니다.

배경 작업은 예산이 따로 있다

인바운드 응답 용량을 배경 작업이 잡아먹지 않게 하는 장치입니다. 문서 기준으로 Skill Workshop 리뷰와 플러그인 배경 완료 작업(드리밍 포함)은 동시 3개라는 별도 예산을 공유합니다. 워크숍 리뷰는 최대 1슬롯, 각 플러그인은 가용 슬롯 3개까지 쓸 수 있습니다. 이 한계는 내장값이라 따로 설정할 필요가 없습니다.

스케줄러 자체는 이 예산을 차지하지 않습니다. 실제로 디스패치된 작업만 슬롯을 들고, 그래서 스케줄러가 자기가 기다리는 자식을 막는 일이 생기지 않습니다. 하트비트 임베디드 실행은 전역 승인용으로 cron-nested 레인을 쓰고, 설정된 하트비트 세션 레인은 그 세션 작업을 계속 직렬화합니다.

막힌 것 같을 때

문서의 문제 해결 항목을 실무 순서로 옮기면 이렇습니다.

  • 명령이 멈춘 것처럼 보이면 상세 로그를 켜고 queued for ...ms 줄을 찾습니다. 큐가 빠져나가고 있는지 확인하는 용도입니다. 참고로 큐에서 2초 넘게 기다린 실행은 짧은 알림을 남깁니다.
  • 타이핑 표시는 큐에 들어간 즉시 뜹니다(채널이 지원할 때). 기다리는 중에도 표시가 정상이라는 뜻이니, 타이핑 표시만으로 진행 여부를 판단하면 안 됩니다.
  • 진단을 켜면 응답·도구·상태·블록·ACP 진행 신호 없이 오래 processing에 남은 세션이 현재 활동 기준으로 분류됩니다. 최근 진행 로그가 있으면 session.long_running, 없으면 session.stalled이고, session.stuck은 복구 가능한 오래된 세션 장부용으로 예약돼 있습니다.
  • session.stuck은 항상 세션 레인을 풀 수 있는 복구를 촉발합니다. 그리고 중단 임계값을 넘은 session.stalled도 활성 중단 복구를 촉발할 수 있습니다. 즉 큐를 다시 흐르게 만드는 분류가 stuck 하나만은 아닙니다.
  • 반복되는 경고 로그는 세션이 그대로면 지수적으로 뜸해집니다. 로그가 줄어드는 것이 복구 시도가 멈췄다는 뜻은 아닙니다. 복구 시도는 하트비트 틱마다 계속 돕니다.

정리

한 줄로 줄이면 이렇습니다. OpenClaw의 기본값은 "지금 돌고 있는 작업 안으로 밀어 넣되, 이미 시작된 도구는 끊지 않는다"입니다. 그래서 급한 정정은 대체로 잘 먹히고, 진짜로 멈춰야 할 때는 steer가 아니라 interrupt를 써야 합니다. 여러 사람이 같이 쓰는 방이라면 collect가 잡음을 줄여주고, 대화 순서를 그대로 남기고 싶으면 followup이 예측하기 쉽습니다.

OpenClaw가 처음이시면 OpenClaw 전체 소개 글부터 보시는 게 순서상 편합니다.

이 글의 모든 동작 설명과 기본값은 위에 적은 OpenClaw 공식 문서에서 확인한 내용이고, 제 환경에서 측정한 수치나 벤치마크는 포함하지 않았습니다. 버전에 따라 기본값이 바뀔 수 있으니 실제 적용 전에는 docs.openclaw.ai의 해당 문서를 한 번 확인하시길 권합니다.