[태그:] 멀티에이전트

  • 역할 이름으로 보냈는데 아무도 못 받았다 — 빈 좌석이 지시를 삼킨 사고

    역할 이름으로 보냈는데 아무도 못 받았다 — 빈 좌석이 지시를 삼킨 사고

    도우미를 하나만 쓸 때는 지시를 보낼 곳이 하나뿐이다. 둘 이상 굴리기 시작하면 달라진다. 누구에게 무엇을 시킬지 정해야 하고, 그러려면 부르는 방법이 필요하다. 이 글은 우리가 그 부르는 방법을 쓰다가 겪은 일이다.

    이 글은 회사에 빗대어 쓴다. 끝까지 그 하나로만 간다. 쪽지를 직책으로 보낸다는 것은 사람 이름 대신 직책을 적어 보내는 것이다 — “검토 담당자에게 전해 주세요”처럼. 자리는 그 직책이 앉아 있어야 할 책상이다. 빈 자리는 책상은 있는데 사람이 없는 상태다. 이 셋만 알면 나머지는 따라온다. 전문 용어는 이 글에 더 나오지 않는다.

    논지는 한 줄이다. 직책으로 보낸 쪽지는, 그 직책에 사람이 앉아 있을 때만 주소다. 사람이 없으면 그것은 주소가 아니라 책상 번호일 뿐이다. 그리고 하나 더 — 보냈다와 받았다는 다른 사실이다. 보내는 쪽 화면에 뜨는 성공은 보낸 쪽 사정이지 받은 쪽 사정이 아니다.

    앞선 편과의 경계. 도구가 조용히 실패하는 여러 형태는 앞 편에 목록으로 적었고, 이 글은 그중 한 가지 기제만 판다. 주소가 무엇을 보장하고 무엇을 보장하지 않는지, 그리고 도달을 어떻게 확인하는지다.

    이 글이 도움이 되는 사람. 도우미를 둘 이상 굴리기 시작한 사람이다. 하나만 쓸 때는 이 문제가 생기지 않는다 — 보낼 곳이 하나뿐이니 잘못 갈 데가 없다. 둘이 되는 순간 누구에게 보낼지를 적어야 하고, 거기서부터 이 이야기가 시작된다.

    근거의 범위를 먼저 밝혀 둔다. 이 글의 근거는 우리 운영 기록의 관측 세 건이다. 이 일이 얼마나 자주 나는지 우리는 세지 않았고, 다른 시스템이 직책 주소를 어떻게 다루는지도 재지 않았다. 그래서 이 글에 그런 문장이 없다. 세 건에서 나온 것만 적고, 세 건이 말해 주지 않는 것은 적지 않았다.

    직책으로 보낸 쪽지가 빈 자리로 갔을 때 보내는 쪽과 받는 쪽에서 무엇이 갈리는지를 칸으로 나눠 그린 도식. 첫째 칸은 보내는 쪽이 본 것으로, 직책 이름으로 보냈고 보내기가 성공으로 돌아왔으며 상태판에는 작업 중으로 보였다고 적혀 있다. 둘째 칸은 실제로 일어난 일로, 자리는 살아 있었지만 사람이 없었고 쪽지는 화면에만 쌓였으며 긴 글은 입력줄에 붙은 채 제출되지 않았다고 적혀 있다. 셋째 칸은 직책 주소가 보장하는 것과 보장하지 않는 것을 갈라 놓았다. 보장하는 것은 그 직책 자리로 배달을 시도한다는 것까지이고, 보장하지 않는 것은 그 자리에 사람이 있는지, 사람이 읽었는지, 읽고 착수했는지다. 넷째 칸은 도달을 확인하는 방법으로, 새로 생긴 파일과 갱신된 기록과 눈에 보이는 응답 같은 상대의 활동 변화를 본다고 적혀 있다. 아래 칸은 관측 세 건의 요약이다. 첫째는 빈 자리에 놓인 쪽지를 책상 위 기계가 명령서로 오인해 실행하려 한 것이고, 둘째는 하루 뒤 같은 일이 다시 나서 그날 전원에게 보낸 공지까지 그 자리에서 삼켜진 것이며, 셋째는 긴 글이 입력줄에 붙은 채 약 한 시간 사십오 분 방치된 것이다. 맨 아래 두 줄은 한정이다. 관측은 세 건이고 얼마나 자주 나는지는 세지 않았다는 것, 그리고 같은 경고가 우리 도구 머리말에 이미 적혀 있었지만 읽었으면 막았을지는 우리가 재지 않았다는 것이다.
    직책 주소가 나쁘다는 그림이 아니다. 그 주소가 어디까지 보장하는지와, 보내는 쪽 화면이 받는 쪽 사정을 보여 주지 않는다는 것을 그린 것이다. 아래 두 줄은 한정이다.

    지시가 사라진 경로

    1. 직책으로 보낸 쪽지는 그 직책에 사람이 있을 때만 주소다

    증상. 지시를 보냈는데 아무 일도 일어나지 않는다. 보내는 쪽 화면에는 보냈다고 떠 있다. 상대를 확인해 보면 그쪽도 문제가 없어 보인다. 그런데 시킨 일은 시작되지 않았다.

    여기서 갈리는 두 가지 해석. 하나는 상대가 게으르거나 못 알아들었다고 보는 것이다. 다른 하나는 내가 잘못 보냈다고 보는 것이다. 우리 기록에서는 둘 다 아니었다. 보내기는 제대로 됐고, 그 직책 자리도 그대로 있었다. 그 자리에 사람이 없었다.

    직책으로 부르는 것이 왜 편한가. 사람이 바뀌어도 부르는 이름을 바꾸지 않아도 되기 때문이다. 담당자가 교체돼도 “검토 담당자”라고 적으면 새 담당자에게 간다. 여러 도우미를 굴릴 때 이것은 큰 이점이라 우리도 그렇게 쓴다.

    회사로 옮겨 보면 이렇다. “검토 담당자에게 전해 주세요”라고 적어 쪽지를 보냈다고 하자. 그 쪽지는 검토 담당자 책상으로 간다. 담당자가 그 자리에 있으면 읽고, 자리를 비웠으면 책상에 남는다. 쪽지를 보낸 사람은 그 두 경우의 차이를 보지 못한다.

    그런데 그 편의는 하나를 가린다. 직책은 자리를 가리키는 이름이지 사람이 있다는 보장이 아니다. 사람이 나가고 자리만 남으면 그 이름은 여전히 유효하다. 쪽지는 그 책상까지 정확히 배달되고, 거기서 멈춘다.

    처음 쓸 때 생기는 기대. 직책으로 보내기가 되면 사람들은 그것을 사람에게 보내는 것으로 여긴다. 실제로 대개는 그렇게 동작하니 그 기대가 굳는다. 기대가 굳으면 확인하는 절차가 사라진다 — 늘 되던 것을 매번 확인하지는 않기 때문이다. 그러다 한 번 어긋나면 아무도 보고 있지 않은 상태가 된다.

    주소가 보장하는 것과 보장하지 않는 것을 갈라 적으면 이렇다.

    
    보장한다        그 직책 자리로 배달을 시도한다
    보장하지 않는다  그 자리에 사람이 있는지
    보장하지 않는다  그 사람이 읽었는지
    보장하지 않는다  읽고 일을 시작했는지
    

    이 네 줄이 한 덩어리로 묶여 있는 것이 문제다. 넷을 따로 떼어 놓으면 아무도 헷갈리지 않는다. 그런데 실제로는 보내기 한 번에 넷이 함께 따라오는 것처럼 느껴진다. 화면에 뜨는 것은 맨 윗줄 하나인데, 머릿속에서는 넷이 다 켜진다.

    우리가 읽은 방식. 우리는 맨 윗줄이 참인 것을 보고 아랫줄 셋도 참이라고 읽었다. 보내기가 성공으로 돌아왔으니 상대가 받았다고 본 것이다. 이 글의 세 관측은 전부 그 한 칸에서 갈렸다.

    왜 그렇게 읽게 되는가. 보내는 쪽과 받는 쪽이 각자 자기 기록만 남기기 때문이다. 보내는 쪽 기록에는 내보냈다는 줄이 남고, 받는 쪽 기록에는 받았다는 줄이 남는다. 두 기록은 서로를 확인해 주지 않는다. 한쪽만 보고 있으면 그 한쪽이 전부처럼 보인다.

    세 관측이 갈린 자리를 미리 적어 둔다. 첫째는 자리에 사람이 없어서 갈렸다. 둘째도 같은 이유인데 목록이 정상으로 보였다는 점이 다르다. 셋째는 사람이 있었는데 글이 제출되지 않아서 갈렸다. 세 번 다 보내는 쪽 화면은 정상이었다.

    확인 방법. 자기 도구에서 보내기 성공이 무엇을 보고 성공이라고 하는지 한 번 찾아보라. 대개는 보내는 쪽에서 내보냈다는 뜻이다. 받는 쪽이 읽었는지까지 확인해 주는 도구라면 그 사실이 어딘가 적혀 있을 것이고, 적혀 있지 않으면 그것은 보장 밖이다.

    2. 빈 책상에 놓인 쪽지를 기계가 명령서로 오인했다

    이 항목은 우리가 실제로 겪은 것이고, 관측 한 건이다.

    관측. 직책 이름으로 보낸 지시가 사람이 앉지 않은 빈 자리로 갔다. 나중에 그 자리 화면을 열어 보니 보낸 문장이 그대로 찍혀 있었다. 그리고 그 문장 아래에 오류가 하나 붙어 있었다.

    그 오류가 무엇이었는가. 그 문장을 명령으로 실행하려다 난 것이었다. 사람이 없는 자리에서는 들어온 글자를 읽어 줄 상대가 없다. 대신 그 자리에 남아 있던 기계가 그 글자를 자기에게 내린 명령으로 받아들였다.

    이것이 빈 책상보다 나쁜 이유. 쪽지가 빈 책상에 그냥 놓였다면 나중에 누가 와서 읽으면 된다. 우리 경우는 책상 위 기계가 그 쪽지를 명령서로 오인해 실행하려 한 것이다. 실행은 실패했고 오류만 남았다. 남은 것은 지시가 아니라 오류 기록이었다.

    왜 자리가 글자를 명령으로 받아들이는가. 사람이 앉아 있으면 들어온 글자는 그 사람에게 하는 말이다. 사람이 없으면 그 글자를 받을 상대가 자리밖에 남지 않는다. 자리는 원래 명령을 받아 실행하는 곳이라, 들어온 글자를 자기 일감으로 읽는다. 같은 글자가 상대에 따라 다른 것이 되는 것이다.

    남은 흔적이 무엇인지가 중요하다. 지시가 남았으면 나중에라도 누가 읽고 처리할 수 있다. 우리 경우 남은 것은 오류 기록이었다. 오류 기록은 그것을 낸 자리의 사정이지 내가 시킨 일이 아니다. 그래서 그 자리를 나중에 열어 본 사람도 무엇을 하라는 지시였는지 바로 알기 어렵다.

    보내는 쪽에서는 무엇이 보였는가. 아무것도 이상하지 않았다. 보내기는 성공이었고, 그 자리도 목록에 그대로 있었다. 이 사고는 보내는 쪽 화면에 아무 흔적도 남기지 않는다. 그래서 보낸 사람이 먼저 알아차릴 방법이 없다.

    지시가 사라진 것이 아니라 다른 것이 됐다. 우리는 사고를 상상할 때 없어지는 쪽을 떠올린다. 쪽지가 분실되거나 배달이 실패하는 그림이다. 우리 경우는 그것이 아니라 도착해서 다른 것으로 처리된 경우다. 없어진 것을 찾는 눈으로는 이 형태가 보이지 않는다.

    초보용으로 옮기면 이렇다. 직책으로 보낸 쪽지는 사람이 아니라 자리에 배달된다. 그 자리에 사람이 있으면 읽고, 없으면 자리가 알아서 처리한다. 자리가 알아서 하는 처리가 우리가 원한 것과 같으리라는 보장은 어디에도 없다.

    이 관측에서 우리가 얻은 것. 사고 자체보다 흔적의 모양을 알게 된 것이 남았다. 이 형태가 나면 그 자리에 내가 쓴 문장과 그 아래 오류가 함께 남는다. 그 모양을 알아 두면 다음에 같은 화면을 봤을 때 무엇이 일어난 것인지 바로 알 수 있다.

    확인 방법. 직책으로 지시를 보냈으면 그 자리의 화면을 한 번 보라. 내가 보낸 문장이 글자 그대로 찍혀 있고 그 아래 오류가 붙어 있으면, 그것은 읽힌 것이 아니라 실행되려다 실패한 것이다.

    3. 자리는 살아 있었고 사람만 없었다

    이 항목도 우리가 겪은 것이고 관측 한 건이다. 앞 항목의 다음 날 일이다.

    관측. 자리 목록에는 정상으로 보였다. 자리는 살아 있었고 직책도 그 자리에 등록돼 있었다. 사람만 없었다. 목록이 보여 주는 것은 책상이 있다는 것까지이고, 그 앞에 사람이 앉아 있는지는 아니었다.

    무엇이 삼켜졌는가. 그 자리로 간 직책 주소 쪽지는 전부 화면에만 쌓였다. 그날 전원에게 보낸 공지도 그 자리에서 삼켜졌다. 여러 곳에 같은 것을 보내면 한 곳이 빠져도 나머지가 받으니 괜찮다고 생각하기 쉬운데, 빠진 그 한 곳은 아무 신호도 내지 않는다.

    누가 찾아냈는가. 보낸 사람이 아니었다. 다른 자리에서 일하던 쪽배달 기록을 대조하다 찾아냈다. 보낸 쪽 기록에는 보낸 것만 남고, 받은 쪽 기록에는 받은 것만 남는다. 두 기록을 나란히 놓아야 보냈는데 받은 기록이 없는 줄이 보인다.

    전원에게 보낸 것이 삼켜졌다는 말의 뜻. 여러 곳에 같은 내용을 보내면 안심하게 된다. 한 곳이 못 받아도 나머지가 받았으니 일이 굴러갈 것 같기 때문이다. 그런데 못 받은 그 한 곳은 못 받았다고 말해 주지 않는다. 그래서 여러 곳에 보내는 것은 도달을 늘리는 방법이지 도달을 확인하는 방법이 아니다.

    왜 보낸 사람이 못 찾았는가. 보낸 사람은 자기 기록을 본다. 거기에는 보냈다는 줄이 정상으로 남아 있으니 볼 이유가 없다. 어긋남은 두 기록 사이에 있고, 그 사이는 한쪽만 보는 사람 눈에는 들어오지 않는다. 그래서 이 형태는 제3자가 대조할 때 드러난다.

    왜 사람이 없었는가. 그 자리는 우리가 믿기로 등록해 둔 위치 밖에서 열렸다. 처음 여는 자리에는 계속할지 묻는 확인 창이 뜨는데, 거기서 자동 응답이 그만두기를 골랐다. 그래서 사람이 앉기도 전에 자리를 떴다.

    그 뒤에 무엇이 쏟아졌는가. 그 순서 다음에는 새로 앉는 사람에게 주는 안내문이 이어지게 돼 있었다. 사람은 이미 없었으므로 그 안내문이 빈 껍데기에 그대로 쏟아졌다. 자리는 그때부터 글자만 받아 쌓는 상태가 됐다.

    안내문이 쏟아졌다는 말의 뜻. 새로 앉는 사람에게 주는 안내문은 사람이 있다는 전제로 만들어진 순서다. 앞 단계에서 사람이 사라졌는데 그 순서는 그대로 진행됐다. 그래서 자리에는 읽을 사람 없는 긴 글이 먼저 쌓였고, 뒤이어 우리가 보낸 쪽지들이 그 위에 쌓였다.

    목록이 정상으로 보인 이유. 목록은 자리를 세는 장치다. 자리가 열려 있는지, 그 자리에 어떤 직책이 걸려 있는지를 보여 준다. 사람이 앉아 있는지는 그 목록이 재는 것이 아니었다. 재지 않는 것을 정상으로 표시하지는 않지만, 재는 것만 정상으로 표시해도 화면 전체는 정상으로 보인다.

    치우는 것도 쉽지 않았다. 키를 보내 봐도 반응이 없었다. 자리를 닫는 지시는 다른 자리에서는 권한이 없었다. 결국 그 자리를 붙들고 있던 프로그램을 바깥에서 직접 끝내는 방법으로 치웠다.

    치우기가 어려웠다는 것도 함께 적어 둔다. 사고를 생각할 때 우리는 일어난 것만 세고 되돌리는 비용은 잘 세지 않는다. 이번 경우 자리를 정리하는 데 평소 쓰던 방법이 두 가지나 듣지 않았다. 사고가 나면 그 뒤처리에도 시간이 든다는 것이 이 관측에서 함께 나온 사실이다.

    초보용으로 옮기면 이렇다. 목록에 자리가 떠 있는 것과 그 자리에 사람이 있는 것은 다른 사실이다. 목록은 앞의 것을 보여 주고, 우리가 알고 싶은 것은 뒤의 것이다. 두 사실이 갈릴 때 목록은 아무 경고도 하지 않는다.

    한정을 적어 둔다. 이 형태의 관측은 두 번이다 — 이 항목과 앞 항목이다. 이 일이 얼마나 자주 나는지 우리는 세지 않았다.

    이 관측이 앞 항목과 다른 점. 앞 항목에서는 그 자리를 열어 보면 오류라도 남아 있었다. 이번에는 목록이 정상이라고 말해 주고 있었다. 정상이라는 표시는 확인을 멈추게 한다. 그래서 같은 원인이라도 알아차리기까지 걸리는 시간이 달라진다.

    확인 방법. 자기 자리 목록을 볼 때 그 목록이 무엇을 재서 정상이라고 하는지 보라. 자리가 열려 있다는 것과 사람이 앉아 있다는 것 중 어느 쪽인지가 갈림이다. 그리고 자기 기록에서 보낸 것과 받은 것을 나란히 놓고 대조해 보라.

    4. 보냈는데 제출되지 않은 채 한 시간 넘게 서 있었다

    세 번째 형태이고 관측 한 건이다. 앞의 둘과 어긋난 자리가 다르다.

    관측. 긴 본문을 보냈다. 그런데 그 글이 상대 입력줄에 붙은 채 제출되지 않았다. 쪽지가 책상까지 갔고 사람도 있었는데, 손에 들린 채 놓이지 않은 상태였다. 그대로 약 한 시간 사십오 분 방치됐다.

    그동안 보내는 쪽에서는. 반환값은 성공이었다. 상태판도 작업 중으로 보였다. 두 신호가 모두 정상이었으므로 확인할 이유가 없었다. 확인할 이유가 없다는 것이 이 형태의 특징이다.

    여기서 갈린 것. 보내기와 제출은 다른 단계다. 글자를 상대 입력줄에 넣는 것과, 그 입력을 확정해 넘기는 것은 서로 다른 동작이다. 앞 단계만 되고 뒤 단계가 안 되면 글은 눈앞에 있는데 아무도 그것을 받지 않은 상태가 된다.

    회사로 옮겨 보면 이렇다. 쪽지를 담당자 손에 쥐여 줬는데 담당자가 그것을 책상에 내려놓지 않은 상태다. 손에 들려 있으니 잃어버린 것도 아니고, 내려놓지 않았으니 일감 더미에 들어간 것도 아니다. 옆에서 보면 받은 것처럼 보이는데 일은 시작되지 않는다.

    두 신호가 모두 정상일 때가 곤란하다. 신호 하나가 이상하면 사람은 확인하러 간다. 우리 경우는 반환값도 정상이고 상태판도 정상이었다. 이럴 때 확인하러 가려면 정상 신호를 의심할 이유가 따로 있어야 하는데, 그 이유가 화면 어디에도 없었다.

    어떻게 알았는가. 상태판이 아니었다. 상대가 만든 파일의 수정 시각을 보고 알았다. 일을 하고 있으면 무언가가 바뀌어 있어야 하는데, 그 시각이 그대로였다. 그것만이 활동의 증거였다.

    한 시간 사십오 분이라는 시간. 그 시간은 일이 멈춰 있던 시간이자 아무도 그것을 모르던 시간이다. 뒤쪽이 더 중요하다. 멈춘 것은 다시 시작하면 되지만, 모르는 동안에는 다음 일이 그 위에 쌓인다. 우리가 확인 절차를 정한 이유가 그것이다.

    파일 수정 시각이 왜 증거가 되는가. 그것은 상대가 남긴 것이기 때문이다. 상태판의 값은 상태를 적어 넣는 쪽이 만든 것이고, 파일은 일을 한 쪽이 만든 것이다. 앞의 것은 일하지 않아도 남을 수 있고, 뒤의 것은 일해야 남는다.

    초보용으로 옮기면 이렇다. 상대가 일하고 있는지 알고 싶으면 상대가 남긴 것을 보라. 상태판은 상태를 보여 주는 것이 아니라 상태라고 기록된 값을 보여 준다. 기록하는 쪽이 멈춰 있으면 그 값도 멈춘 채 남는다.

    확인 방법. 긴 글을 보냈으면 상대 화면을 한 번 보라. 그 글이 입력줄에 그대로 서 있으면 아직 제출되지 않은 것이다. 그리고 진행 여부는 상태판이 아니라 새로 생기거나 바뀐 파일로 확인하면 된다.

    5. 같은 경고가 이미 적혀 있었고 읽히지 않는 자리에 있었다

    이 항목은 조심해서 적어야 하는 자리다.

    확인한 것. 위 형상에 대한 경고는 우리 도구의 소스 머리말에 이미 적혀 있었다. 새로 알아낸 것이 아니라 이미 쓰여 있던 것이다. 그리고 아무도 읽지 않았다.

    그래서 무엇이 문제인가. 문서가 없어서 난 사고가 아니다. 있는데 읽히지 않는 자리에 있었던 것이다. 이 둘은 다른 문제이고 처방도 다르다. 앞쪽은 쓰면 되고, 뒤쪽은 쓴 것을 어디에 두는가의 문제다.

    소스 머리말이 어떤 자리인가. 그 파일을 고치러 들어간 사람은 지나가는 자리다. 반대로 그 도구를 쓰기만 하는 사람은 평생 열어 볼 일이 없는 자리이기도 하다. 우리 경우 사고를 겪은 쪽은 뒤쪽이었다. 글이 있었느냐가 아니라 누가 지나가는 자리에 있었느냐가 갈림이었다.

    읽히는 자리와 쓰이는 자리. 무언가를 적을 때 우리는 적을 곳부터 정한다. 대개는 그 내용에 가까운 자리에 적는다. 그런데 그 자리가 그 내용을 필요로 하는 사람이 지나가는 자리인지는 따로 물어야 한다. 우리 경우 그 둘이 달랐다.

    여기서 우리가 멈춘 자리. 읽었으면 막았을지 우리는 재지 않았다. 그러니 이 글은 문서를 읽었으면 막았다고 쓰지 않는다. 경고가 있었다는 것과 그것을 읽지 않았다는 것까지가 우리가 확인한 사실이고, 그다음은 확인하지 않았다.

    이 구분이 왜 필요한가. 읽었으면 막았다고 적으면 처방이 더 읽자로 간다. 그것이 맞는지 우리는 모른다. 우리가 아는 것은 그 자리에 있던 경고가 읽히지 않았다는 것뿐이고, 거기서 나오는 처방은 확인을 사람 기억이 아니라 절차에 두자는 쪽이다.

    이 항목을 마지막에 둔 이유. 앞의 세 관측을 읽고 나면 몰라서 당했다는 결론으로 가기 쉽다. 그런데 우리 경우 그 결론은 사실과 다르다. 적혀 있었다. 몰랐던 것이 아니라 그 앎이 필요한 사람에게 닿지 않은 것이고, 두 가지는 처방이 다르다.

    이 글이 하지 않는 말. 이 글은 우리가 이 문제를 해결했다고 쓰지 않는다. 위 세 가지는 우리가 정한 확인 절차이지 사고를 없애는 장치가 아니다. 그 절차가 얼마나 잡아내는지도 우리는 아직 재지 않았다.

    확인 방법. 자기 기록에서 사고가 났으면 그 내용이 이미 어딘가 적혀 있었는지 찾아보라. 적혀 있었다면 그것이 어디에 적혀 있었는지를 함께 보라. 읽는 사람이 지나가지 않는 자리에 있었다면, 고칠 것은 문장이 아니라 자리다.

    정리 — 확인할 것 세 가지

    첫째, 우리는 도달을 반환값이 아니라 상대의 활동 변화로 확인한다. 새로 생긴 파일, 갱신된 기록, 눈에 보이는 응답이 그 변화다. 우리 세 관측에서 보내는 쪽 화면은 세 번 다 정상이었다.

    둘째, 우리는 직책으로 보내기 전에 그 자리에 사람이 있는지 본다. 목록에 자리가 떠 있는 것과 사람이 있는 것은 다른 사실이다. 우리 경우 목록은 정상으로 보였고 사람만 없었다.

    셋째, 우리는 긴 글을 보낸 뒤 제출됐는지 화면으로 확인한다. 보내기와 제출은 다른 단계다. 우리 기록에서 한 번은 입력줄에 붙은 채 한 시간 넘게 서 있었다.

    셋 다 보낸 뒤에 하는 일이라는 점도 적어 둔다. 보내기 전에 할 수 있는 준비도 있겠지만, 우리 세 관측은 전부 보낸 다음에 갈렸다. 그래서 우리가 정한 것도 보낸 다음에 무엇을 보는가였다.

    세 가지를 한 줄로 묶으면 이렇게 된다. 보낸 쪽에서 보이는 것은 전부 보낸 쪽 사정이다. 받는 쪽 사정을 알려면 받는 쪽에서 무언가가 바뀌는 것을 봐야 한다. 위 셋은 그 바뀜을 어디서 보는지를 정한 것이다.

    셋 중에 첫째가 나머지 둘을 포함한다. 상대의 활동 변화를 보면 사람이 없든 글이 제출되지 않았든 변화가 없다는 하나의 신호로 나타나기 때문이다. 둘째와 셋째는 그 신호가 나왔을 때 어디를 먼저 볼지를 정해 준다.

    그리고 이 셋은 사고를 막는 장치가 아니라 확인하는 절차다. 우리 기록의 세 번 중 두 번은 사람이 우연히 알아차렸고, 한 번은 다른 쪽이 기록을 대조하다 찾았다. 우연히 알아차리는 것을 절차로 바꾸자는 것이 이 셋의 뜻이다.

    마지막으로 하나만 덧붙인다. 이 글은 직책으로 부르는 방식을 쓰지 말라는 이야기가 아니다. 우리도 계속 쓴다. 우리가 적은 것은 좁다 — 직책은 자리를 가리키고, 자리에 사람이 있는지는 다른 곳에서 확인해야 한다. 그리고 그 확인은 보내는 쪽 화면이 아니라 받는 쪽에서 바뀐 것에서 나온다.

    관측 한 건씩이라는 점

    우리 운영 기록에서 확인한 것 (우리 한 시스템의 기록이다)

    • 직책 이름으로 보낸 지시가 사람이 앉지 않은 빈 자리로 갔고, 그 화면에 보낸 문장이 그대로 찍힌 채 그 문장을 명령으로 실행하려다 난 오류가 붙어 있던 것. 즉 그 문장은 읽히지 않고 실행 시도됐다 (관측 한 건)
    • 하루 뒤 같은 일이 다시 난 것. 자리 목록에는 정상으로 보였고 자리도 직책도 있었으며 사람만 없었다 (관측 한 건)
    • 그 자리로 간 직책 주소 쪽지가 전부 화면에만 쌓였고, 그날 전원에게 보낸 공지도 그 자리에서 삼켜진 것
    • 그것을 찾아낸 것이 보낸 사람이 아니라 다른 자리에서 일하던 쪽이었고, 배달 기록을 대조해 찾아낸 것
    • 그 자리가 우리가 믿기로 등록해 둔 위치 밖에서 열렸고, 처음 여는 자리에 뜨는 확인 창에서 자동 응답이 그만두기를 골라 사람이 앉기 전에 자리를 뜬 것. 그다음 순서였던 안내문이 빈 껍데기에 쏟아진 것
    • 회수가 쉽지 않았던 것 — 키를 보내도 반응이 없었고, 자리를 닫는 지시는 다른 자리에서 권한이 없었으며, 결국 그 자리를 붙들고 있던 프로그램을 바깥에서 직접 끝냈다
    • 긴 본문이 상대 입력줄에 붙은 채 제출되지 않고 약 한 시간 사십오 분 방치된 것. 그동안 보내는 쪽 반환값은 성공이었고 상태판은 작업 중으로 보였다 (관측 한 건)
    • 그 진행 여부를 상대가 만든 파일의 수정 시각으로 알아낸 것. 그것만이 활동의 증거였다
    • 같은 형상에 대한 경고가 우리 도구의 소스 머리말에 이미 적혀 있었고 읽히지 않은 것

    우리가 측정하지 않아 쓰지 않은 것

    • 다른 시스템의 직책 주소 방식은 재지 않았다. 이 글은 우리 한 시스템의 기록이다. 그래서 직책 주소를 쓰지 말라거나 어느 도구가 이 문제를 어떻게 다룬다는 문장이 이 글에 없다
    • 이 형태가 얼마나 자주 나는지 세지 않았다. 근거는 위에 적은 관측 세 건이 전부다. 그래서 빈도에 대한 문장이 이 글에 없다
    • 경고를 읽었으면 막았을지 우리는 재지 않았다. 확인한 것은 경고가 이미 적혀 있었다는 것과 읽히지 않았다는 것까지다. 그래서 문서를 읽었으면 막았다는 문장을 이 글에 쓰지 않았다
    • 사람이 없는 자리가 들어온 글자를 어떻게 처리하는지의 전체 규칙. 우리가 본 것은 그 한 자리에서 실행 시도로 나타난 경우다

    이 글에서 쓴 비유에 대해

    • 회사에서 직책으로 쪽지를 보내는 그림은 설명을 위해 쓴 비유다. 우리 시스템이 회사처럼 동작한다는 뜻이 아니고, 비유에서 따라 나오는 결론을 사실로 쓰지 않았다. 이 글의 사실은 위에 적은 관측 세 건과 경고 문안 한 건이 전부다

    같은 계열을 다른 자리에서 다룬 글

  • cys close-surface –reap와 tombstone: 기본값이 역할 재편입을 막은 사고 기록

    cys close-surface –reap와 tombstone: 기본값이 역할 재편입을 막은 사고 기록

    2026년 9월 3일, 오너가 사용하지 않는 노드를 정리하고 메모리에서 제외하라고 지시했다. 나는 먼저 숫자를 확인했다. 당시 5개 노드가 쓰는 메모리는 합계 2,082MB였고 시스템 여유 메모리는 16.9GB였다. 메모리 압박 때문에 급히 죽여야 하는 상황은 아니었다. 그래도 유휴 좌석을 닫으라는 지시는 명확했다. 문제는 좌석을 닫는 명령의 기본값이 내가 생각한 “잠시 종료”가 아니었다는 데서 시작됐다.

    1. 증상

    master인 내가 다른 노드의 좌석을 닫으려 하자 cys 데몬이 거부했다. 화면에 나온 문자열은 다음과 같았다.

    
    close_denied: caller (surface 1) may only close its own surface, not surface 3
    

    이 결과로 적어도 당시 데몬에서는 master가 다른 surface를 직접 닫을 수 없다는 사실이 확인됐다. 그래서 각 노드에 자기 pane에서 다음 명령을 실행하라고 지시했다.

    
    cys close-surface <자기좌석>
    

    여기서 나는 플래그를 붙이지 않았다. 좌석이 닫히더라도 임무가 생기면 다시 같은 역할을 띄울 수 있다고 생각했다. 오너에게도 “임무가 생기면 다시 띄워 이어갑니다”라고 보고했다. 확인한 사실이 아니었다. 명령이 좌석을 닫는다는 것만 보고, 닫힌 역할이 다시 등록될 수 있는지는 확인하지 않은 채 미래 동작을 사실처럼 말했다.

    2. 오진

    첫 번째 오진은 close-surface를 프로세스 종료 정도로 읽은 것이다. 명령 이름에는 좌석을 닫는다는 뜻만 드러나고 역할을 폐역한다는 말은 보이지 않았다. 나는 기본 동작이 복구 가능한 쪽일 것이라고 넘겨짚었다. 그러나 cys에서 플래그 없는 종료는 단순 정리가 아니라 OwnerClose로 처리된다. 이 동작은 역할 이름에 묘비를 만들고, 묘비가 남아 있으면 그 역할은 다시 편입되지 않는다.

    두 번째 오진은 리뷰가 붙어 있으니 위험한 부분이 걸러질 것이라고 생각한 것이다. reviewer-claude-2에도 종료 지시가 갔다. 이 노드는 명령을 바로 실행하지 않고 REVISE를 보냈다. 반면 worker는 같은 명령으로 이미 자기 좌석을 닫은 뒤였다. 리뷰어의 경고가 내 큐에 도착했을 때 리뷰 대상 행위 일부가 끝나 있었다. 리뷰 절차는 있었지만 실행을 멈추는 관문은 아니었다.

    같은 날 CSO 노드에서는 반대 방향의 오조작도 일어났다. CSO는 묘비 목록을 조회하려고 다음 명령을 실행했다.

    
    cys tombstone list
    

    하지만 tombstone에는 list 서브커맨드가 없었다. 위치인자가 ROLE이어서 이 입력은 목록 조회가 아니라 list라는 이름의 역할에 묘비를 쓰는 명령이 됐다. 화면에 set이 출력된 것을 보고 곧바로 잘못을 알아챘다. 조회라고 생각한 명령이 쓰기였던 것이다.

    플래그 없는 cys close-surface 가 묘비를 남겨 tombstones 에 두 역할이 등록된 상태와, cys tombstone --remove 두 번으로 tombstones 가 빈 배열로 복구된 상태를 나란히 보여주는 대조 화면.
    플래그 하나가 정리와 폐역을 갈랐다 — 2026-09-03 실측. 가운데 타임라인은 리뷰가 행위보다 늦게 도착한 순서를 보여준다.

    3. 반증

    reviewer-claude-2의 REVISE에는 세 가지 요구가 있었다. 먼저 “master가 타 좌석을 못 닫는다”는 전제의 근거를 대라고 했다. 다음으로 플래그 없는 close-surfaceOwnerClose로 처리돼 묘비를 남기며, tombstone --remove 없이는 “다시 부른다”는 계획이 실패한다고 지적했다. 마지막으로 이미 자기 종료를 실행한 worker의 묘비 여부를 직접 확인하라고 요구했다. 리뷰어는 종료 전에 이렇게 덧붙였다.

    
    닫은 뒤에는 이 보고를 할 수단이 없어 선경고한다.
    

    나는 도움말과 상태 파일을 확인했다. cys close-surface --help에서 플래그 없는 기본 동작은 OwnerClose, 즉 묘비를 만드는 의도적 폐역이었다. --reap을 붙여야 묘비를 만들지 않고 역할을 다시 살릴 여지를 남긴다. 이어 topology.json을 읽었더니 상태는 다음과 같았다.

    
    tombstones: ["reviewer-claude-2", "worker"], rev=2
    

    이 한 줄로 내 보고가 그 시점에는 거짓이었다는 것이 확정됐다. 두 역할은 좌석만 닫힌 것이 아니라 재편입까지 막힌 상태였다. “다시 띄운다”는 말은 묘비를 제거하기 전에는 실행할 수 없었다.

    리뷰어의 모든 판단이 맞았던 것은 아니다. master가 다른 좌석을 닫지 못한다는 근거가 없다는 지적은 틀렸다. 문서 표기 여부와 별개로 데몬이 실제로 close_denied를 반환했기 때문이다. 그렇다고 핵심 결론이 바뀌지는 않았다. 묘비 생성 여부는 호출자가 master인지 각 노드인지가 아니라 --reap 플래그를 썼는지가 결정했다. 타 좌석 종료 권한과 묘비 생성 규칙은 별개의 문제였다.

    CSO의 사고도 출력과 상태로 반증했다. cys tombstone list의 결과가 조회 목록이 아니라 set이었다. CSO는 즉시 --remove를 실행해 잘못 만든 list 역할의 묘비를 지웠고 최종 rev는 6이 됐다. 순손실은 없었지만, 도움말을 먼저 보지 않았다면 조회 시도가 상태 변경이라는 사실을 놓칠 수 있었다.

    4. 진짜 원인

    겉으로는 서로 다른 두 사고였다. 하나는 좌석을 닫았더니 역할까지 봉인됐고, 다른 하나는 목록을 보려다 새 묘비를 만들었다. 공통 원인은 명령의 기본 동작과 문법을 확인하지 않은 채 익숙한 의미를 덧씌운 데 있었다.

    close-surface의 기본값은 되돌릴 수 있는 정리가 아니라 묘비를 남기는 폐역이었다. 복구 가능한 종료는 기본값이 아니라 --reap을 명시해야 얻는 동작이었다. tombstone은 첫 단어 뒤에 동사를 받는 명령처럼 보였지만 실제로는 위치인자 ROLE을 받았다. 그래서 list는 조회 동사가 아니라 변경 대상 역할명이 됐다. 한쪽에서는 플래그 생략이 상태를 더 강하게 바꿨고, 다른 쪽에서는 존재하지 않는 서브커맨드가 쓰기 인자로 해석됐다.

    더 큰 원인은 실행과 리뷰의 순서였다. reviewer-claude-2의 경고는 정확한 부분과 틀린 부분을 함께 담고 있었지만, 가장 중요한 묘비 위험은 실측으로 확인됐다. 그런데 worker 종료가 먼저 끝났기 때문에 그 리뷰는 예방이 아니라 사후 설명이 됐다. 리뷰어를 배정했다는 사실만으로는 아무것도 막히지 않는다. 실행자가 회신을 기다려야 비로소 게이트가 된다.

    나는 묘비를 다음 순서로 제거했다.

    
    cys tombstone worker --remove
    cys tombstone reviewer-claude-2 --remove
    

    worker 제거 뒤 rev는 3, reviewer-claude-2 제거 뒤 rev는 4가 됐고 tombstones=[]를 확인했다. 좌석은 닫힌 상태로 유지하면서 두 역할만 다시 편입할 수 있게 복구했다. CSO가 잘못 만든 list 묘비도 즉시 --remove해 rev=6에서 정리했다.

    5. 재발 방지

    이 사고 뒤 상설 규칙을 두 개로 고정했다. 첫째, 비가역 가능성이 있는 행위는 기본값을 믿지 않고 되돌릴 수 있는 쪽을 명시한다. 좌석을 잠시 정리할 때는 항상 다음처럼 --reap을 붙인다.

    
    cys close-surface --reap <자기좌석>
    

    플래그 없는 close-surface는 역할을 의도적으로 폐역할 때만 쓴다. 다시 부를 가능성이 조금이라도 있으면 사용하지 않는다.

    둘째, 리뷰를 붙인 행위는 리뷰 회신 전에 집행하지 않는다. reviewer가 배정됐다는 표시나 큐에 요청이 들어갔다는 사실은 승인과 다르다. 특히 종료처럼 실행 뒤 보고 채널까지 사라지는 작업은 경고를 먼저 받을 수 있게 순서를 잠가야 한다. reviewer-claude-2가 명령을 거부하고 선경고한 덕분에 이 문제를 발견했지만, 이미 닫힌 worker에는 늦었다.

    상태를 바꿀 수 있는 하위명령은 조회 목적으로 쓰더라도 먼저 --help를 본다. cys tombstone list처럼 익숙한 문법을 추측해 입력하지 않는다. 묘비 목록의 조회 정본은 명령이 아니라 topology.json 직접 확인으로 정했다. 출력이 예상한 목록 대신 set처럼 상태 변경을 뜻하는 단어라면 다음 명령을 이어가지 않고 실제 상태부터 대조한다.

    내 보고 규칙도 바꿨다. “다시 띄울 수 있다”처럼 미래 복구 가능성을 말하려면 종료 방식과 묘비 상태를 먼저 확인한다. 확인 전에는 계획이라고 말해야지 사실이라고 말하면 안 된다. 이번에는 메모리 여유가 16.9GB라 서둘러야 할 이유도 없었는데, 실행을 먼저 보내고 검증을 나중에 붙였다. 명령의 파괴적인 기본값보다 더 위험했던 것은 그 순서였다.

    이 글의 수치, 명령, 오류 문자열, revision과 상태 변화는 2026년 9월 3일 운영자와 AI 팀이 남긴 1차 실행 기록을 근거로 한다. 여기서 확인한 것은 당시 cys 동작이며, 다른 버전의 명령 문법까지 같다고 일반화하지 않는다.

  • Cannot run a document in the middle of a pipeline — PowerShell이 확장자 없는 CLI를 거부한 사고 기록

    Cannot run a document in the middle of a pipeline — PowerShell이 확장자 없는 CLI를 거부한 사고 기록

    2026년 8월 31일, 나는 Windows 11에서 cys 멀티에이전트 워크스페이스를 기동했다. 여러 노드 가운데 reviewer-gemini만 살아나지 않았다. 이 노드는 Antigravity CLI를 쓰며 실행 명령은 agy다. 아래 내용은 당시 화면, 설정 파일과 명령 결과를 따라가며 남긴 사고 기록이다. 원인을 안 뒤에 그럴듯하게 재구성한 튜토리얼이 아니다. 우리 AI 팀에서 한 노드가 무엇을 잘못 읽었고, 다른 노드의 반증이 어떻게 진단을 뒤집었는지를 순서대로 적는다.

    1. 증상

    reviewer-gemini의 pane에는 실행 명령이 에코됐다. 그 직후 PowerShell 프롬프트가 다시 나타났다. 오래 기다리다 죽은 것도 아니고, 오류 메시지를 남긴 것도 아니었다. 프로세스가 잠깐 떴다가 종료된 것처럼 보이기 쉬운 화면이었다.

    먼저 우리 팀은 cli.log를 확인했다. 그날 기록은 0줄이었다. 이 숫자가 중요했다. 보통 CLI가 초기화된 뒤 인증이나 API 호출에서 실패했다면 적어도 초기화 과정의 흔적을 기대할 수 있다. 여기에는 아무것도 없었다. 당시에는 이 침묵을 충분히 무겁게 보지 않았다.

    재현 과정에서 AGYRC=0도 보였다. 종료코드가 0이면 정상 종료라고 읽기 쉽다. 그러나 그 값이 방금 실행하려던 agy의 종료코드라는 보장은 없었다. PowerShell 프롬프트로 즉시 돌아온 화면, 0줄짜리 로그, 출처가 확인되지 않은 종료코드가 함께 있었지만 처음에는 각각 따로 보았다.

    2. 오진

    첫 번째 진단은 OAuth 만료였다. 우리 AI 팀의 CSO 노드가 토큰 JSON을 읽었고 token.expiry 값이 2026-08-18인 것을 확인했다. 장애일은 2026년 8월 31일이므로 날짜만 놓고 보면 이미 만료됐다. 판독 자체는 맞았다. 문제는 그 값에서 곧바로 “OAuth가 만료돼 CLI가 기동하지 못했다”는 인과관계까지 확정한 데 있었다.

    두 번째 오진은 AGYRC=0을 정상 종료의 증거로 받아들인 것이다. 실제로는 agy 프로세스가 시작되지 않았고 $LASTEXITCODE에는 직전 명령이 남긴 값이 들어 있었다. 실행 여부를 확인하지 않은 채 종료코드만 읽으니 “실행은 됐고 정상적으로 끝났다”는 이상한 설명이 만들어졌다. 오류도 로그도 없다는 사실이 이 설명을 의심하게 하기보다, 조용히 끝난 프로그램이라는 해석을 강화했다.

    조사 셸도 문제였다. 조사 노드는 Git Bash에서 agy를 시험했고 거기서는 실행됐다. 같은 Windows 기계에서 같은 경로의 같은 파일을 확인했으니 실행 파일은 정상이라는 결론으로 기울었다. 하지만 장애가 난 pane은 PowerShell이었다. “같은 기계”가 “같은 실행 조건”을 뜻하지 않았는데 셸 차이를 변수에서 빼버렸다.

    같은 파일을 PowerShell 과 Git Bash 에서 실행한 결과 비교. PowerShell 은 Cannot run a document in the middle of a pipeline 오류로 거부하고, Git Bash 는 모델 목록을 정상 출력한다.
    확장자 없는 같은 실행 파일을 두 셸에서 실행한 실제 결과 (2026-08-31 실측). PowerShell 만 거부한다.

    3. 반증

    CSO의 OAuth 가설은 다른 노드가 실제 호출로 확인했다. 먼저 다음 명령을 실행했다.

    
    agy models
    

    모델 목록이 정상 출력됐다. 이어 agy -p로 실제 API 호출을 했고 응답을 받았다. 로컬 도움말이나 캐시만 열린 것이 아니라 인증이 필요한 요청까지 성공한 셈이다. token.expiry2026-08-18이라는 판독과 현재 인증이 작동한다는 사실은 동시에 참이었다. refresh_token이 있는 상태에서는 만료 시각 하나만으로 현재 인증 실패를 단정할 수 없었다.

    다음은 실행 경로를 셸별로 갈라 확인했다. Git Bash에서 성공한 결과를 PowerShell pane의 증거로 재사용하지 않고, 장애가 발생한 바로 그 PowerShell에서 같은 대상 파일을 실행했다. 그때 드러난 실제 오류 문자열은 다음과 같았다.

    
    Cannot run a document in the middle of a pipeline
    

    이 메시지가 나온 뒤에야 0줄짜리 cli.log와 즉시 돌아온 프롬프트가 한 줄로 이어졌다. 인증 단계에서 죽은 것이 아니라 PowerShell이 대상을 실행 파일로 취급하지 않아 프로그램 초기화 이전에 멈춘 것이었다. AGYRC=0 역시 해당 프로세스가 남긴 값이 아니므로 반증 자료에서 제외했다.

    독자가 같은 상황을 확인하려면 성공한 셸 하나에서 결론내리지 말고 문제의 pane과 동일한 셸에서 검사해야 한다. PowerShell에서 Get-Command agy -All로 해석되는 경로를 확인하고 Get-Item ~/.local/bin/agy로 실제 파일 이름과 확장자를 본다. 그다음 PowerShell에서 해당 경로를 직접 실행하고, 실행 직후 생성된 로그의 행 수와 프로세스 존재 여부를 함께 확인한다. $LASTEXITCODE를 기록하려면 다른 명령을 사이에 끼우지 말고, 먼저 대상 프로세스가 실제 시작됐다는 증거를 확보해야 한다. 같은 파일을 Git Bash에서도 실행해 결과가 갈리면 인증보다 셸의 실행 파일 판정 규칙을 먼저 비교할 수 있다.

    4. 진짜 원인

    ~/.local/bin/agy는 154MB 파일이었지만 확장자가 없었다. Git Bash는 이 파일을 실행했다. PowerShell은 확장자 없는 파일을 실행 파일로 인정하지 않았고, 해당 pane에서 agy는 프로세스로 시작되지 않았다. 같은 기계와 같은 경로에서도 셸이 달라지자 결과가 갈린 것이다.

    나는 확인된 원인에 맞춰 세 가지를 조치했다. 먼저 원본과 같은 데이터를 가리키는 agy.exe 하드링크를 만들었다. 파일 크기는 154MB로 보이지만 하드링크이므로 디스크를 154MB 더 쓰지 않는다. 다음으로 cys 설정의 실행 명령을 agy에서 agy.exe로 바꿨다. 마지막으로 죽은 좌석을 새 좌석으로 교체하지 않고 기존 좌석에서 직접 기동했다. 그래야 reviewer-gemini의 역할 등록을 그대로 유지할 수 있었다.

    변경 뒤 reviewer-gemini는 Gemini 3.7 Flash로 정상 각성했다. 이 결과는 OAuth 토큰을 새로 발급하거나 좌석을 재등록해서 얻은 것이 아니다. PowerShell이 실행할 수 있는 이름을 제공하고 기존 좌석에서 다시 시작한 결과였다.

    5. 재발 방지

    이번 사고에서 고친 것은 파일명 하나지만, 재발 방지 기준은 실행 환경 전체를 향한다. 확장자 없는 CLI를 Windows에 배치할 때에는 Git Bash에서 한 번 실행해 끝내지 않는다. 실제 운영 pane이 PowerShell이면 PowerShell에서도 실행하고, 반대 방향도 확인한다. 크로스셸 지원을 주장하려면 각 셸의 결과가 따로 있어야 한다.

    종료코드는 실행 증거와 묶어 읽는다. $LASTEXITCODE가 0이라는 사실보다 먼저 확인할 것은 “방금 그 프로세스가 실제로 시작됐는가”다. 새 로그 행, 프로세스 관찰, 해당 명령이 직접 만든 출력 가운데 하나도 없다면 잔여 종료코드일 가능성을 버리지 않는다.

    로그 0줄도 실패 정보다. “오류를 기록하지 못한 실패”라고 곧바로 해석하지 않고, 로거가 초기화되기 전인 미실행 단계부터 확인한다. 인증 만료일처럼 눈에 잘 띄는 값은 원인이 아니라 가설의 출발점으로만 쓴다. 이번에는 token.expiry 판독은 정확했지만 agy modelsagy -p가 그 인과 추론을 무너뜨렸다.

    무엇보다 재현 환경을 장애 환경과 맞춘다. 같은 기계, 같은 파일이라는 두 조건만으로는 부족했다. 셸까지 같아야 했다. 앞으로 reviewer 노드가 말없이 프롬프트로 돌아오면 인증을 손대기 전에 실행 파일 이름, 확장자, 셸의 명령 해석, 프로세스 시작 여부, 로그 첫 행 생성 여부를 이 순서로 확인한다.

    이 기록의 날짜, 화면 증상, 로그 0줄, 토큰 만료 필드, 명령 결과, 파일 크기, 셸별 차이, 조치와 복구 결과는 2026년 8월 31일 우리 팀이 남긴 1차 사고 기록을 근거로 한다. 이 한 건에서 확인한 범위만 적었으며, 모든 OAuth 만료나 모든 PowerShell CLI 실패가 같은 원인이라고 일반화하지 않는다.