Skip to Content
사용자 가이드Features크로스 세션 프로토콜

크로스 세션 프로토콜

이 페이지는 Qwen Code 세션이 아닌 상태에서 크로스 세션 메시징에 참여하려는 프로그램(음성 프론트엔드, 릴레이 데몬, 빌드를 감시하는 스크립트 등)을 위한 계약서입니다. 세션이 레지스트리에 무엇을 기록하는지, inbox가 연결에서 무엇을 읽어들이는지, 그리고 무엇을 다시 보내는지를 설명합니다. 여기 있는 모든 내용은 스키마 버전 1, 프레임 버전 1 기준으로 현재 코드가 실제로 동작하는 방식이며, 마지막 섹션에서 무엇이 어떻게 변경될 수 있는지 안내합니다.

프로세스 경계를 넘나드는 모든 값은 도착 시 신뢰할 수 없는 것으로 간주되어 읽는 측에서 검증합니다. 이 페이지에서 필드가 특정 형태를 “가져야 한다”고 설명하는 경우, 해당 형태를 만족하지 않는 값은 오류로 거부되지 않고 단순히 무시됩니다.

1. 세션 레지스트리

실행 중인 세션은 하나의 레코드를 게시합니다:

$QWEN_HOME/sessions/<pid>.json (directory 0700, file 0600) $QWEN_HOME/sessions/<pid>-<8 hex>.json (여러 세션을 호스팅하는 프로세스)

$QWEN_HOME의 기본값은 ~/.qwen입니다. 파일 이름은 작성자의 PID를 키로 사용합니다. bare PID 형태이거나, PID 뒤에 대시와 등록 시 생성된 8개의 소문자 16진수 문자가 붙은 형태입니다(아래의 “하나의 프로세스에서 여러 레코드” 참조). pid 필드가 파일 이름의 PID 접두사와 일치하지 않는 레코드는 무시됩니다. 정규 10진수 형태로 비교되므로, 0으로 패딩된 이름은 어떤 것과도 일치하지 않습니다.

{ "schemaVersion": 1, "pid": 41337, "procStart": "a1b2c3d4-…-boot-uuid:8895124", "pidNs": 4026531836, "sessionId": "8e016be8-5b48-4c13-ad22-1f5326ae64ac", "cwd": "/home/me/project", "name": "project-3f", "startedAt": 1788959000000, "qwenVersion": "0.23.0", "kind": "tui", "ipcPath": "/run/user/1000/qwen-socks/41337.sock", "ipcToken": "c0ffee…64 hex…" }
필드의미
schemaVersion항상 1. 읽는 측은 더 높은 버전의 레코드는 건너뛰며 삭제하지 않습니다.
pid작성자의 프로세스 ID. 파일 이름의 키가 되는 PID와 동일해야 합니다: bare 형태의 경우 전체 이름, minted 형태의 경우 -<8 hex> 접미사 앞의 숫자.
procStartLinux에서는 <boot id>:<process start ticks>(/proc/sys/kernel/random/boot_id/proc/<pid>/stat의 22번째 필드). 그 외의 환경에서는 null. PID 재사용을 방지하며, 이 홈 디렉토리를 공유하는 다른 머신에서 작성된 레코드도 ��지합니다.
pidNsLinux에서 /proc/self/ns/pid의 inode 번호. 그 외의 환경에서는 null. 읽는 측은 자체 네임스페이스의 레코드만 나열하고 정리합니다.
sessionId세션의 ID. /clear/resume은 동일한 PID 아래에서 이를 교체하므로, 매번 전송 전에 레코드를 다시 읽으십시오.
cwd등록 시점의 작업 디렉토리.
name표시 이름. cwd의 basename(유니코드 문자, 마크, 숫자, ., _, -; 최대 32코드포인트)에서 파생되며, 뒤에 -sha256(sessionId)의 첫 두 개의 16진수 문자가 붙습니다. 작성자가 직접 지정한 경우 해당 값을 사용합니다. 고유하지 않습니다.
startedAt에폭 밀리초. 최신순이 나열 순서이며, 동일한 시작 시간을 가진 세션 사이의 우선순위 기준입니다.
qwenVersion자유 텍스트 또는 null.
kind등록 주체: tui(터미널에 있는 사용자), headless, serve, external. 소문자 ASCII, 숫자, 대시만 사용 가능하며 최대 16자. 그 외의 값은 읽을 때 무시됩니다. 필드가 없으면 해당 필드보다 오래된 작성자로 간주되며 tui로 읽힙니다. 리스팅을 위한 레이블이며, 절대로 자격 증명이 아닙니다. 아래 참조.
ipcPathinbox 소켓. 바인딩된 상태에서만 존재합니다. 이 필드가 없으면 발견 가능하지만 메시지 전송은 불가능함을 의미합니다.
ipcToken16진수 문자 64개. ipcPath로의 연결이 auth 라인에 제시하는 값입니다. 이 필드가 없으면 inbox가 인증을 요구하지 않음을 의미합니다(구 버전 빌드의 레코드).

레코드는 자기 보고입니다. 레코드의 모든 필드는 해당 레코드가 설명하는 프로세스가 직접 작성한 것이므로, name, cwd, kind는 주장일 뿐이며 읽는 측이 의존할 수 있는 사실이 아닙니다. 발신자가 무엇을 할 수 있는지 결정하는 어떤 로직도 이 필드들을 읽지 않습니다. 이는 연결이 제시하는 것(§3)과 수신 세션의 자체 정책(§6)에 따라 결정됩니다. kind는 리스팅이 세션을 정직하게 그룹화할 수 있도록 설정하십시오. 이것이 어떠한 권한도 부여할 것이라고 기대하지 마십시오.

자체 레코드 작성. 발견되기를 원하는 외부 프로세스(qwen sessions ps로 나열되고, send_message에서 주소 지정 가능하며, 영수증을 수신할 수 있으려면) 는 자체 레코드를 동일한 방식으로 작성합니다. 자체 pid, 동일한 방식으로 계산한 procStartpidNs, 직접 생성한 sessionId(임의의 UUID), kind: "external", name(직접 지정하거나 동일한 방식으로 파생된 값. 표시 시 한 줄로 평탄화되고 길이 제한이 적용됨), 그리고 직접 바인딩한 inbox의 ipcPath + ipcToken(§2)을 포함합니다. 같은 디렉토리의 임시 파일에 작성한 후 rename으로 대상 파일을 덮어쓰십시오. 파일은 0600으로 생성하고, 심볼릭 링크를 통한 작성은 거부하십시오. 종료 시 레코드를 제거하십시오. 프로세스가 사라진 레코드는 다음에 나열을 수행하는 세션이 정리하지만, procStart가 PID가 단순히 재사용된 것이 아님을 증명하는 경우에만 정리합니다.

읽기. 디렉토리를 읽을 수 있는 모든 것은 토큰을 포함하여 모든 레코드를 읽을 수 있습니다. 세션을 발견할 수 있다는 것과 해당 세션에 인증할 수 있다는 것은 설계상 동일한 권한입니다. 모델이나 로그가 볼 수 있는 곳에 ipcToken을 출력하지 마십시오.

활성 상태. 레코드는 다음 조건을 모두 만족할 때 활성 상태입니다. 파일 이름이 <pid>.json 또는 <pid>-<8 hex>.json 형태이고 그 PID 접두사가 pid와 일치하고, pidNs가 읽는 측의 것과 같고, procStart 내의 boot id가 읽는 측의 것과 같거나(또는 procStartnull이고), PID가 동일한 시작 ticks로 활성 상태여야 합니다. ipcPath가 있는 활성 레코드도 도달 가능하다고 광고되기 전에 반드시 연결이 성공해야 합니다. 소켓 파일은 크래시 이후에도 남아있을 수 있습니다.

Refs. 표시용 핸들은 ref = sha256(sessionId)[0:6]을 사용합니다. 두 세션이 동일한 name을 공유할 수 있으므로, 발신자가 입력하는 주소 문법은 name, name [ref], [ref] 또는 단독 ref이며, 모호한 name은 추측이 아닌 오류로 처리됩니다.

하나의 프로세스에서 여러 레코드. 데몬에 의해 생성되거나 에디터나 다른 클라이언트에 의해 직접 구동되는 모든 qwen --acp 자식은 첫 번째 세션부터 세션당 하나의 레코드를 <pid>-<8 hex>.json 이름으로 작성합니다. 접미사는 등록 시 생성되며 절대 변경되지 않습니다. 그 아래에서 세션 ID가 교체되는 것은 레코드의 수정이지 이름 변경이 아닙니다. 이 중 모든 레코드는 동일한 ipcPath를 가집니다. 프로세스가 모든 세션에 대해 하나의 inbox를 바인딩하고 각 프레임의 toSessionId로 세션을 구분하기 때문입니다. 따라서 항상 toSessionId를 전송하십시오: 이 필드 없이 그러한 프로세스에 도달하는 프레임은 misaddressed로 응답합니다. 해당 프로세스가 의미할 수 있는 단일 세션이 없기 때문입니다. 활성 상태, 정리, 네임스페이스 및 boot 가드는 bare 이름과 정확히 동일하게 레코드를 읽습니다. PID/파일 이름 일치 확인만 다르며, 그것은 전체 이름이 아닌 접미사 앞의 숫자에 대해 pid를 비교하는 것뿐입니다.

2. inbox 소켓

세션당 하나의 UNIX 도메인 소켓을 다음 중 먼저 바인딩 가능한 경로에 생성합니다:

  1. $XDG_RUNTIME_DIR/qwen-socks/<pid>.sock
  2. $TMPDIR/qwen-socks-<16 hex>/<pid>.sock
  3. /tmp/qwen-socks-<16 hex>/<pid>.sock

디렉토리는 0700, 소켓은 0600입니다. 경로가 103바이트를 초과하면 건너뜁니다. PID 기반 이름이 이미 활성 리스너에 의해 점유된 경우(두 PID 네임스페이스가 런타임 디렉토리를 공유하는 경우), 세션은 그 옆에 <pid>-<8 hex>.sock을 대신 바인딩합니다. 피어는 절대 소켓 경로를 직접 유도하지 않습니다. 레코드에서 ipcPath를 읽습니다.

연결은 개행으로 구분된 JSON을 전달합니다. 한 줄에 하나의 객체, UTF-8 인코딩입니다. UTF-16 코드 유닛 기준으로 1 MiB를 초과하는 단일 줄은 연결을 차단합니다. 연결 후 30초 이내에 파싱 가능한 줄을 완성하지 못하면 연결이 차단됩니다. 잘못된 줄이 전송되어도 기한은 연장되지 않습니다. 리스너는 최대 64개의 동시 연결을 수용합니다.

예상되는 교환은 연결당 하나의 메시지입니다. 연결, auth 라인과 프레임을 한 번의 쓰기로 전송, half-close, 피어가 연결을 닫을 때까지 대기합니다. 수신자는 같은 연결에서 절대 응답하지 않습니다. 응답해야 할 내용은 자체 ipcPath로의 별도 연결로 전달됩니다.

3. auth 라인

대상 레코드에 ipcToken이 있는 경우, 첫 번째 줄은 다음과 같아야 합니다:

{ "msgV": 1, "type": "auth", "token": "<token>" }

세 가지 종류의 토큰이 허용되며, inbox는 어떤 토큰을 수신했는지 기억합니다:

제시된 토큰inbox의 결론효과
대상 레지스트리 레코드의 ipcToken일반 피어정책 및 모드 일치 적용(§6)
대상 자체 환경의 QWEN_CODE_MESSAGING_TOKEN해당 세션이 시작한 프로세스기본 일치 설정으로 전달. origin="own-process"
qwen sessions controllers add로 생성된 컨트롤러 토큰 qpc_<64 hex>사용자가 신뢰하는 프로그램기본 일치 설정으로 전달. origin="controller"와 함께 grant의 레이블 포함

auth 라인이 아닌 첫 번째 줄, 또는 세 가지 중 어느 것에도 일치하지 않는 토큰을 제시하는 첫 번째 줄은 연결을 조용히 차단합니다. 레코드에 ipcToken이 없는 경우 auth 라인을 전송하지 마십시오. 구 버전 inbox는 이를 알 수 없는 프레임 유형으로 읽고 건너뜁니다. 따라서 auth 라인을 먼저 전송하는 것은 항상 안전합니다.

여기서 아무것도 _발신자_를 인증하지 않습니다. 토큰은 연결이 허용된다는 것을 증명할 뿐, 누가 열었는지는 증명하지 않습니다. from, fromName, fromMode 및 레코드의 모든 필드는 주장일 뿐입니다.

이것이 신뢰 모델의 전부입니다. 사용자의 세션을 구동하도록 허용된 프로그램은 사용자가 직접 생성하여 해당 프로그램에만 부여하는 컨트롤러 토큰을 받습니다. 이것이 메시지가 전달되는 것과 검토를 위해 대기하는 것의 차이를 만듭니다. kind: "external"을 설정하거나 익숙해 보이는 name을 사용한다고 해서 어떠한 이점도 얻지 못합니다.

4. 사용자 프레임

{ "msgV": 1, "msgId": "5f1d0c9e-3b2a-4e8f-9c7d-1a2b3c4d5e6f", "type": "user", "from": "/run/user/1000/qwen-socks/40011.sock", "replyToken": "<my own ipcToken>", "fromName": "project-3f", "fromMode": "prompting", "toSessionId": "8e016be8-…", "priority": "next", "message": { "role": "user", "content": "build finished, 0 failures" } }
필드규칙
msgV숫자. 1 이하여야 합니다. 더 높은 값은 무시됩니다.
msgId^[A-Za-z0-9][A-Za-z0-9_-]{0,63}이며 all로 정규화되지 않아야 합니다(대시 제거, 소문자 변환 금지). 각 메시지마다 새로운 UUID를 사용하십시오. 수신자는 이미 처리한 ID를 기억하며 재전송된 메시지에 대해 이전 판단을 반복합니다.
type"user".
from자체 ipcPath(있는 경우). 영수증이 전송될 위치입니다. 이 필드가 없으면 영수증을 받지 않습니다.
replyToken자체 ipcToken. 수신자가 영수증을 발신자에게 인증할 수 있도록 합니다.
fromName표시 이름. 한 줄로 평탄화되며 최대 200자.
fromMode"prompting"(사용자가 각 작업을 검토) 또는 "bypass"(일부 작업이 검토 없이 적용). 이 필드가 없으면 “아무것도 주장하지 않음”을 의미하며, 검토를 위해 보류됩니다(§6).
toSessionId레코드에서 읽은 sessionId. 다른 ID를 보유한 수신자는 misaddressed로 응답합니다. 항상 전송하십시오.
priority"now" 또는 "next". 그 외의 값은 "next"로 읽힙니다. 향후 인터럽트 경로를 위해 유지되며, 현재는 수신자가 둘 다 다음 턴을 위해 큐에 저장합니다.
messagerole"user"여야 하며, content는 비어있지 않은 문자열이어야 합니다.

알 수 없는 필드는 무시됩니다.

5. 전달 상태 프레임

수신자는 각 결과에 대해 하나의 제어 프레임으로 메시지의 결과를 보고합니다. 이는 메시지의 from으로 전송되며 replyToken으로 인증됩니다:

{ "msgV": 1, "msgId": "<fresh id>", "type": "control", "action": "delivery_status", "status": "held", "origMsgId": "5f1d0c9e-…", "from": "/run/user/1000/qwen-socks/41337.sock", "reason": "Your message is held for the recipient user to review …" }
status발생 시점대응 방법
held사용자가 검토하도록 보류됨. 재시도 시 및 큐에 추가할 수 없는 방출 시에도 반복됨.대기. 사용자 결정 또는 만료가 뒤따릅니다.
delivered모델을 위해 큐에 추가됨.없음. 읽혔다는 증거는 아닙니다.
denied사용자가 검토 후 거부.재전송하지 마십시오.
refused세션의 정책이 피어 메시지를 거부. 아무도 보지 못함. 첫 영수증에만 해당.중단. 해당 사용자에게 다른 방법으로 접근하십시오.
expired보류된 메시지가 대기 시간을 초과했거나, 세션이 메시지를 읽지 못한 채 종료되었거나, 세션이 종료되는 동안 도착함. held 또는 delivered 뒤에 올 수 있음.여전히 중요한 경우 나중에 재전송하십시오.
misaddressedtoSessionId가 해당 주소의 세션과 일치하지 않음.레지스트리를 다시 읽으십시오.
dropped정책이 실행되기 전에 inbox가 거부(§6).미전송으로 처리. 반복해서 재시도하지 마십시오. 중요한 내용은 이후 메시지에 한 번만 포함.

dropped 영수증은 두 개의 추가 필드를 포함합니다. dropReasonrate-limited, duplicate, queue-full 중 하나입니다. droppedMsgIds는 동일한 영수증이 처리하는 최대 256개의 추가 ID를 나열합니다. 버스트는 개별 영수증이 아닌 하나의 영수증으로 응답되므로, 발신자는 손실된 모든 메시지를 단일 프레임에서 종료 상태로 전환합니다. 이 두 필드는 다른 상태에서는 의미가 없으며 무시됩니다.

reason은 사용자를 위한 자유 텍스트입니다. 영수증의 순서는 연결 간에 보장되지 않습니다. 상태 전이로 적용하십시오:

pending → held | delivered | denied | refused | expired | misaddressed | dropped held → delivered | denied | expired | misaddressed delivered → expired | misaddressed

그 외의 경우는 중복이며 무시해야 합니다. 전송한 적 없는 ID에 대한 영수증은 노이즈입니다. 무시하십시오. 영수증은 수신자 측에서 최선을 다해 전송됩니다. 송신 한도 초과 또는 from의 장애는 영수증을 조용히 유실시키므로, 발신자는 응답을 받지 못하는 상황을 견딜 수 있어야 합니다.

자체 inbox는 메시지를 보낸 세션들로부터 이 프레임을 수신합니다. 전송만 하는 경우에도 inbox를 바인딩하고 from을 반드시 지정하십시오. 그렇지 않으면 위 결과 중 어떤 것도 알 수 없습니다.

6. 수신자가 메시지를 처리하는 방식

다음 순서로 처리됩니다:

  1. 수락. 발신자당: 30개의 버스트, 이후 2초당 하나의 메시지. 전체 발신자 합산: 32개의 버스트, 이후 초당 하나의 메시지. 발신자는 프레임에서 자체 이름을 지정하므로, 해당 이름을 순환시키면 첫 번째 한도에서는 새로운 할당을 얻지만 두 번째 한도에서는 얻지 못합니다. 30초 이내에 다른 세션에서 전송된 동일한 본문은 duplicate로 처리됩니다. 세션이 시작한 프로세스와 신뢰된 컨트롤러는 이 검사에서 면제되며, 다른 모든 발신자와 동일하게 속도 제한이 적용됩니다. 거부된 메시지는 절대 보류되지 않고, 전달되지 않으며, 기록에 남지 않습니다. 따라서 버스트가 지나갈 때까지 기다린 후 재시도하는 발신자도 여전히 도착합니다.
  2. 처리된 ID. 게이트가 이미 결정한 msgId는 이전 판단을 반복합니다.
  3. 정책. agents.crossSessionInboundaccept, hold, refuse로 설정된 경우 해당 설정이 우선합니다. 설정되지 않은 경우: 세션이 시작한 프로세스 또는 신뢰된 컨트롤러는 수락됩니다. 그 외의 경우, fromMode가 수신자와 동일한 검토 클래스를 나타내는 경우에만 수락되며, fromMode가 없는 경우를 포함하여 다른 모든 경우에는 보류됩니다.
  4. 보류. 최대 50개의 메시지가 대기합니다. 버퍼가 가득 찬 상태에서 도착하는 메시지는 이미 보류된 메시지를 축출하는 대신 queue-fulldropped 처리됩니다. 보류된 메시지는 agents.crossSessionHeldExpiry(1m, 5m, 10m, never. 기본값 5m) 이후 만료됩니다. 사용자는 /peers에서 방출 또는 거부할 수 있으며, 모드 변경 시 대기열이 재평가됩니다.
  5. 큐. 수락된 메시지는 세션의 입력 큐에 추가됩니다. 피어로부터의 메시지는 최대 50개까지 보유됩니다. 큐가 가득 찬 경우에도 queue-fulldropped 처리됩니다.

발신자가 한도를 직접 시도해 알아낼 필요는 없습니다. Qwen Code 세션은 주소별로 한도를 미러링하며, 자체 전송 전에 한도를 초과하는 경우 전송을 거부하고 모델에게 대신 배치 처리하도록 지시합니다.

모델은 전달된 메시지를 다음과 같이 봅니다:

<cross_session_message from="/run/user/1000/qwen-socks/40011.sock" name="project-3f"> build finished, 0 failures </cross_session_message>

뒤에 발신자의 권한을 명시하는 알림이 따릅니다. origin="own-process" 또는 origin="controller" controller="<label>"는 수신자가 연결에서 제시한 정보를 바탕으로 추가하며, 프레임에서 가져오지 않습니다. 컨트롤러의 레이블은 fromName이 아닌 사용자가 생성한 grant에서 가져옵니다. content 내에서 엔벨프처럼 보이는 태그는 무력화됩니다.

7. 호환성

  • 읽는 측은 알 수 없는 필드를 무시합니다. 레코드나 프레임에 필드를 추가하는 것은 호환성을 깨는 변경이 아닙니다.
  • schemaVersionmsgV는 기존 필드의 구조가 변경되는 경우에만 증가합니다. 읽는 측은 자신이 아는 것보다 높은 버전의 프레임은 무시하고 레코드는 건너뜁니다. 그러한 레코드를 삭제하지는 않습니다.
  • 새로운 status 값이 추가될 수 있습니다. 알 수 없는 값은 “전이 없음”으로 처리하고 계속 대기하십시오. 인식할 수 없는 kind도 동일하게 처리합니다. 표시는 하되 수정하지 마십시오.
  • 예고 없이 변경될 수 있는 상수: 버스트 및 속도 수치, 보류 한도 및 만료 옵션, 1 MiB 줄 길이 제한, 30초 줄 deadline, 64개 연결 한도.

8. 아직 결정되지 않은 사항

  • 이름 양보. 하나의 디렉토리에 있는 두 세션이 동일한 name을 등록할 수 있으며, 현재는 ref로만 구분됩니다. 활성 이름에 양보하는 등록 및 세션이 이름을 변경했음을 피어에게 알리는 제어 프레임은 모두 아직 구현되지 않았습니다.
  • 동일 이름 보고. qwen sessions pslist_agents는 여전히 충돌하는 레코드에 대해 플래그를 표시하지 않습니다.
  • ACP 구동 세션으로의 수신 메시지. ACP를 통해 프로그램이 구동하는 세션(데몬이 생성했는지 여부와 관계없이)은 등록 및 전송이 가능하지만, 자신에게 전송된 모든 메시지에 대해 refused로 응답합니다. 보류는 사람에게 묻는 질문이며, 그 세션을 대신해 보류 목록을 감시하는 사람은 없기 때문입니다. 보류된 메시지가 해당 세션(클라이언트 또는 데몬의 자체 API)에서 어디에 표시되어야 하는지는 아직 열려 있는 문제입니다.
  • 하나의 inbox 뒤의 세션들은 모든 피어에게 하나의 발신자이다. 여러 세션을 호스팅하는 프로세스는 하나의 from 주소로 전송하므로, 수신자의 발신자별 할당량 및 중복 창(§6)은 해당 프로세스의 모든 세션이 공유합니다. 바쁜 형제가 다른 세션의 할당량을 소모할 수 있으며, 하나의 세션에 방금 전송한 본문은 창 내에서 형제 세션에게 반복할 수 없습니다. 세션별 회계는 프레임에서 주장하는 필드를 신뢰해야 하며, §3의 신뢰 모델은 이를 허용하지 않습니다.
Last updated on