증상 (jinwon-int/fleet-skills#18 intake 게이트 Phase A, r12)
- 태스크 생성 후 4분 30초 만에
exceeded_requeue_limit으로 dead-letter: "task exceeded stale threshold 2700000ms" (45분 문턱이 4.5분 만에 발화)
- 워커(dungae)는 핸들러를 정상 실행 중이었고, heartbeat는 "cannot heartbeat task while status is queued" 409로 반복 거부(requeue 루프의 부산물)
- 재현 2회: r12와 동일 조건 r11 이전 라운드(r7~r11) 전부 동일 실패 양상
근본 원인
packages/broker/src/workers/broker-worker-client.ts의 matchingPiriSessionDirs() 매칭 술어:
if (entry.name !== sessionDir && !entry.name.includes(taskId) && !taskId.includes(entry.name)) continue;
세 번째 조건 taskId.includes(entry.name) 때문에, piri 워크 루트에 존재만 하면 되는 짧은 이름의 디렉터리가 실제 task id 부분 문자열이면 전부 매칭됩니다.
실제 사례: dungae의 /var/lib/a2a-runner/piri-tasks/s (한 글자 디렉터리, 2026-08-19 잔재). "s"를 포함하는 모든 task id와 매칭되어 그 안의 artifacts/piri-progress.jsonl mtime(2026-08-19T00:30:47.987Z)이 heartbeat의 lastProgressAt으로 브로커에 보고됐습니다.
연쇄:
resolveTaskStalenessSignalMs()는 lastProgressAt을 최우선 정합 신호로 신뢰 (broker-status-predicates.ts:29 — "progress is authoritative")
- task 레코드의
lastProgressAt = 9일 전 → stale 스윕이 생성 즉시 45분 초과 판정
- requeue → queued↔running 진동 →
BROKER_MAX_REQUEUE_ATTEMPTS=2 소진 → dead-letter
실증 (인과 확정)
s 디렉터리 격리(/root/a2a-quarantine/s.stray-20260829T0335KST) 후 동일 매니페스트·동일 워커로 재디스패치(r13) 즉시 성공 — heartbeat 정상, verdict approve, 리뷰 게이트 통과
- r12 레코드의
lastProgressAt(2026-08-19T00:30:47.987Z)과 격리된 디렉터리 내 progress 파일 mtime이 밀리초까지 정확히 일치
제안 수정
- client (근본):
matchingPiriSessionDirs()에서 taskId.includes(entry.name) 절 제거 또는 가드 추가 — (a) sanitize된 sessionDir와의 정확 일치 + taskId 포함(contains)만 허용, 또는 (b) 최소 길이(예: ≥ 16자) + a2a- 프리픽스 요구. 진행 파일은 세션 디렉터리 네이밍 규약(a2a-<worker>-<taskId>-analysis)으로 재구성 가능하므로 contains 방향(entry.name.includes(taskId))만 남겨도 커버 충분
- broker (방어 심층): heartbeat의 client 제공
lastProgressAt을 task.createdAt 이전 값이면 기각(또는 clamp). 기존 monotonic 가드(broker-task-checkpoint.ts:186)는 "이전 값보다만 새로우면" 통과시켜 과거 시점 자체는 막지 못함
운영 조치 이력 (2026-08-29 KST)
- dungae
s 격리 완료. 플릿 전수 점검: 다른 노드 단문 디렉터리 0건 (jingun k3canary2는 실 task id와 부분 일치 불가한 9자라 저위험 — 세션 디렉터리 규약으로의 개명 권고, soonwook piri-<epoch> 계열은 매칭 불가한 무해 잔재)
크로스링크
증상 (jinwon-int/fleet-skills#18 intake 게이트 Phase A, r12)
exceeded_requeue_limit으로 dead-letter: "task exceeded stale threshold 2700000ms" (45분 문턱이 4.5분 만에 발화)근본 원인
packages/broker/src/workers/broker-worker-client.ts의matchingPiriSessionDirs()매칭 술어:세 번째 조건
taskId.includes(entry.name)때문에, piri 워크 루트에 존재만 하면 되는 짧은 이름의 디렉터리가 실제 task id 부분 문자열이면 전부 매칭됩니다.실제 사례: dungae의
/var/lib/a2a-runner/piri-tasks/s(한 글자 디렉터리, 2026-08-19 잔재). "s"를 포함하는 모든 task id와 매칭되어 그 안의artifacts/piri-progress.jsonlmtime(2026-08-19T00:30:47.987Z)이 heartbeat의lastProgressAt으로 브로커에 보고됐습니다.연쇄:
resolveTaskStalenessSignalMs()는lastProgressAt을 최우선 정합 신호로 신뢰 (broker-status-predicates.ts:29 — "progress is authoritative")lastProgressAt= 9일 전 → stale 스윕이 생성 즉시 45분 초과 판정BROKER_MAX_REQUEUE_ATTEMPTS=2소진 → dead-letter실증 (인과 확정)
s디렉터리 격리(/root/a2a-quarantine/s.stray-20260829T0335KST) 후 동일 매니페스트·동일 워커로 재디스패치(r13) 즉시 성공 — heartbeat 정상, verdict approve, 리뷰 게이트 통과lastProgressAt(2026-08-19T00:30:47.987Z)과 격리된 디렉터리 내 progress 파일 mtime이 밀리초까지 정확히 일치제안 수정
matchingPiriSessionDirs()에서taskId.includes(entry.name)절 제거 또는 가드 추가 — (a) sanitize된 sessionDir와의 정확 일치 + taskId 포함(contains)만 허용, 또는 (b) 최소 길이(예: ≥ 16자) +a2a-프리픽스 요구. 진행 파일은 세션 디렉터리 네이밍 규약(a2a-<worker>-<taskId>-analysis)으로 재구성 가능하므로 contains 방향(entry.name.includes(taskId))만 남겨도 커버 충분lastProgressAt을task.createdAt이전 값이면 기각(또는 clamp). 기존 monotonic 가드(broker-task-checkpoint.ts:186)는 "이전 값보다만 새로우면" 통과시켜 과거 시점 자체는 막지 못함운영 조치 이력 (2026-08-29 KST)
s격리 완료. 플릿 전수 점검: 다른 노드 단문 디렉터리 0건 (jingunk3canary2는 실 task id와 부분 일치 불가한 9자라 저위험 — 세션 디렉터리 규약으로의 개명 권고, soonwookpiri-<epoch>계열은 매칭 불가한 무해 잔재)크로스링크