레슨 7은 경계에서 멈췄어
레슨 7의 킬 스위치는 로컬 도구 루프를 다음 경계에서 끝내. 라운드가 시작될 때나, 도구를 부르기 직전. 이미 날아간 모델 요청은 절대 안 자르니까, 그건 맞아. 그런데 그 경계가 못 덮는 게 둘 남아 있었어. 도구 자체가 몇 분씩 돌 수 있고, 그걸 기다리는 멈춤은 멈춤이 아니야. 그리고 턴이 도는 중에 서버가 재시작될 수 있는데, 그건 아무도 안 누른 멈춤이야. 2026년 10월에 둘 다 닫혔어.
Bash 명령은 호출과 함께 끝나
로컬 브리지 여섯은 Bash 도구 호출을 subprocess.run(command, shell=True, timeout=...) 으로 돌렸어. 시간이 다 되면 셸만 죽고, 셸이 띄운 건 하나도 안 죽어. 두뇌는 '시간 초과' 를 읽는데 명령은 계속 돌았어. 한 번은 재시도가 첫 사본 옆에 두 번째 사본을 띄웠고, 한 번은 고아가 된 명령이 두뇌가 그새 끝낸 보고서를 덮어썼어. 2026-10-04 아빠 판정은 그걸 멈추라는 거였어.
이제 variants/bash_commands.run_bash_command 가 명령마다 자기 세션에서 시작해. 그래서 셸이 자기 프로세스 그룹의 우두머리가 되고, 그걸 띄운 작업 스레드가 0.2초씩 끊어서 기다려. 이유 셋 중 먼저 오는 것 하나로 명령을 끝내. 호출의 시간이 다 됐거나, 그 대화에 운영자의 킬 스위치가 올라갔거나, 백엔드가 꺼지는 중이거나. 끝낼 땐 프로세스 그룹 전체를 끝내. 먼저 SIGTERM 을 보내고, 유예(3초, 꺼질 땐 0.3초) 동안 git 은 잠금 파일을 지우고 셸은 trap 을 돌려. 그다음이 SIGKILL 이야. 결과는 셋 중 뭐였는지와 그룹이 죽었다는 걸 말하고, 멈추기 전까지의 출력을 붙여.
빠지면 무너지는 디테일이 넷이야.
- 신호는 스레드 하나만 보내. 명령을 띄운 스레드만, 그것도 셸을 거두기 전에만 신호를 보내. 거두지 않은 우두머리는 자기 그룹 번호를 붙들고 있어서, 번호를 다시 쓰는 다른 그룹에 신호가 떨어질 일이 없어. 킬 스위치와 종료는 깃발만 올려.
- 멈춤 결과는 전부
Error:로 시작해. 그래야 stateful 체크포인트에 안 들어가. 멈춘 명령은 끝난 단계가 아니니까. - 안 죽는 걸 끝없이 기다리지 않아. SIGKILL 뒤에도 출력이 열려 있으면, 호출은 돌아와서 뭔가 아직 돌고 있을 수 있다고 말해.
- 일부러 뒤로 돌린 작업은 호출보다 오래 살아. 이젠 서버 재시작도 넘겨 살아. 예전엔 서버의 프로세스 그룹에 들어 있어서, launchd 가 재시작 때마다 말 한마디 없이 죽였어.
Claude 는 이 경로에 없어. Claude 의 Bash 도구는 Claude Code 자기 거거든. 짐작하지 않고 같은 날 실제로 확인했어. 31.7초짜리 sleep 에 2초 시간 제한을 걸었더니 시간 초과로 돌아오면서 sleep 은 사라져 있었고, 킬 스위치는 요청 0.3초 만에 돌던 sleep 을 끝냈어.
재시작은 도는 턴한테 유예를 빚져
백엔드는 꺼질 때 떼어낸 chat 파이프라인한테 30초 유예를 준다고 늘 말해 왔어(레슨 5). 2026-10-08 cold review 가 실제로 무슨 일이 일어나는지 쟀어. launchctl kickstart -k 가 런처한테 SIGTERM 을 보내고, 종료 시간 제한이 없으니 launchd 는 5초 뒤 런처를 죽이고 자식들은 고아로 남겼어. 게다가 런처 자기의 3초 자르기 때문에, 백엔드 유예가 1초쯤 지난 시점에 도는 턴이 전부 끝났어. 파이프라인의 정리 코드는 한 번도 안 돌았어. 말로만 있던 유예가 단 한 번도 지켜진 적이 없었던 거야.
이제 런처의 종료 기한은 턴 유예와 uvicorn 의 창을 다 기다리되, 그 작업의 종료 시간 제한에서 2초 뺀 것보다는 절대 안 넘어. plist 가 유예를 자르게 돼 있으면 시작할 때 경고하고, 사무실 plist 는 종료 시간 제한을 45초로 받았어. 꺼질 땐 먼저 Master Chief 대화를 기다리던 턴을 '중단됨' 으로 거둬. 그래야 유예가 곧 잘라 버릴 모델 호출을 새로 시작하는 턴이 안 생겨. 그리고 엔진이 받지 못한 턴은 모든 클라이언트에서 돌아와. Telegram 은 tg-<chat>-<message> 를 턴 id 로 보내고, 엔진이 다시 응답하기 시작하면 턴 상태를 한 번 묻고, 엔진에 그 턴이 없을 때만 같은 id 로 다시 보내. WebUI 는 webui-<uuid> 를 보내고, 시작 전에 연결이 끊겨 엔진이 받지 못한 턴은 그 말을 대기열로 돌려놓고 붙잡아 둬. 기다리는 Chief 턴은 이제 404가 아니라 pending 으로 읽혀. 거기서 404가 나면 다시 보내라는 신호가 돼서 턴이 두 번 돌았거든.
같은 문을 모든 기기에
레슨 7의 조작은 WebUI 에서 시작했어. 2026-10-07에 네이티브 앱이 그걸 제대로 받았어. Stop 은 서버의 멈춤 의도를 저장해서, 연결이 끊기거나 다시 켜진 폰이 아빠가 일부러 멈춘 턴을 다시 보내는 일이 없어. 대기열은 대화마다 있고 고칠 수 있어. 보내기는 방향 틀기나 줄 서기를 고를 수 있고, 대체 보내기 전엔 턴이 가라앉길 기다려. 전해졌는지 불확실한 방향 틀기는 서버의 새 기록과 맞춰 본 다음에야 풀어. 입력창은 지금 도구, 개수, 흐른 시간, 글 꼬리를 보여주고, 다른 기기에서 도는 턴도 대화를 다시 열면 똑같이 보여. 비공개 대화의 조작은 메모리에만 있고, 아빠가 그 대화를 떠난 뒤에도 그 Stop 은 지켜져.