Skip to content

fix(server): macOS support for the idle gjc lane + live detection — remove comm/lsof/proc/locale Linux assumptions - #2

Open
thisisjun786 wants to merge 27 commits into
devswha:fix/live-lane-idle-gjc-sessionsfrom
thisisjun786:feat/idle-lane-macos
Open

fix(server): macOS support for the idle gjc lane + live detection — remove comm/lsof/proc/locale Linux assumptions#2
thisisjun786 wants to merge 27 commits into
devswha:fix/live-lane-idle-gjc-sessionsfrom
thisisjun786:feat/idle-lane-macos

Conversation

@thisisjun786

@thisisjun786 thisisjun786 commented Jul 12, 2026

Copy link
Copy Markdown

Scope (updated — grew beyond the original detection port)

This PR started as the macOS port of the idle gjc lane and now carries four layers, in commit order:

  1. macOS live/idle detection (supersedes and includes PR fix(server): macOS support for live gjc session detection — remove Linux-only lsof/proc/locale assumptions #1): lsof comm filters, ps snapshot ancestry (no /proc), batched lsof -d cwd, launchd no-locale LANG defence, argv-based gjc evidence for the idle lane.
  2. Idle first-message UX: idle rows open a main-area waiting view (single MainTakeover union), first message relays via the tower, the view auto-resolves to the real conversation once its transcript appears. Pure transition helpers in idleTransition.ts with 25 unit tests. Multi-candidate cases never auto-switch (banner + manual pick).
  3. UX polish: per-row subtitles → tooltips, non-tmux gjc sessions hidden from the live list (they stay in the internal live set so read-only banners/LIVE badges keep protecting them), post-send boost polling (5s→1s for 30s), spawn-name pre-validation, workspace-root autocomplete for the spawn form (scope=spawn, absolute paths — home-relative contract of the files panel untouched).
  4. Review-driven hardening (independent architect-lane review, verdict REQUEST CHANGES — all blocking findings fixed here):
    • HIGH tmuxId ($N generation token) is now REQUIRED for send/kill; omitting it used to skip the same-name replacement guard entirely. Missing/malformed → 400; stale → 409. Client fails closed when it has no token.
    • HIGH gjc argv evidence narrowed to argv[0] or a bun/node first script argument — vim /tmp/gjc and shell command strings no longer promote a pane to a lineage claim.
    • MED transcript-lane lsof is scoped to proven gjc pid trees (one ps snapshot + descendant BFS) instead of every bun/node process — protects the 4MiB/4s subprocess guard on node-heavy hosts.
    • MED waiting view detects generation replacement even when the new generation appears only as a real transcript row; the 15s resolving timeout now actually bounds background project refetches (recovery navigation stays).
    • MED workspace spawn suggestions return absolute paths, so a home/workspace name collision can no longer spawn somewhere other than what the picked suggestion displayed.
    • LOW autocomplete no longer resurrects stale responses after the input is cleared.

Verification

  • 88 unit tests pass (tsx --test, server 52 / client 36), tsc --noEmit both configs, ESLint clean on touched files.
  • Deployed on a real macOS host (launchd + tailscale serve): live rows for tmux sessions with generation tokens, idle rows for pre-first-message panes, waiting-view auto-transition observed end-to-end (~14s pre-boost, ~1-2s detection latency after a relayed send with boost polling), 400/403/409 contract probed over HTTP.
  • A wiring regression found right after deploy (3-column ps snapshot fed into the 2-column parser zeroed all detection) is fixed and pinned by a dedicated test.

Open questions for the maintainer (deliberately NOT changed here)

  • i18n: new UI strings follow this branch's existing hard-coded-Korean convention; the reviewer flagged the repo's useTranslation layer. Happy to move them to locale keys if you want that direction.
  • resolveAllowedRoots direct-child symlink trust (pre-existing): the reviewer suggests an explicit decoy-root contract instead of trusting every top-level symlink. Separate PR if you agree.
  • External CLI attach by name only (pre-existing): could carry $N like the gjc lane; separate PR.
  • History is append-only per review discipline; squash-merge (or ask for a rebase) if you prefer a clean history.

Merge order: #1 first then this, or this alone (it strictly contains #1; note #1 alone still has the broad lsof -c bun -c node cost — its fix lives here).

gjc 라이브 레인이 macOS에서 항상 0건이었다:
- lsof -c gjc: comm 매칭이라 bun 런타임으로 도는 gjc(comm=bun)를 못 찾음
  → -c gjc -c bun -c node로 확장 (SESSION_FILE_RE가 실질 필터, -c는 비용 절감용)
- /proc/<pid>/stat, /proc/<pid>/cwd: macOS에 /proc 없음
  → 조상 체인은 ps -eo pid=,ppid= 스냅샷 1회, holder cwd는 배치 lsof -d cwd로 대체
  (둘 다 Linux에서도 동일 동작 — 플랫폼 분기 없음)

외부 CLI 레인: macOS ps -eo comm은 실행파일 풀 경로를 찍어 claude/codex
descendant 분류가 전부 미스 → comm basename 정규화.

검증: 유닛 30/30, tsc 통과, 실기(darwin) getLiveGjcSessions() 5건 검출
(lineage/cwd/null 클레임 분류 정상).
launchd/systemd는 로케일 env를 안 준다. 비UTF-8 로케일의 tmux는 출력을
새니타이즈해 \t 필드 구분자가 '_'로 치환되고 비ASCII 경로가 이스케이프됨
→ parseTmuxPanes가 전 행 파싱 실패 → 모든 라이브 세션 tmuxName:null
(실측: launchd 서버에서 UI 전 세션 열람 전용으로 강등, 전송 불가).

runCommand spawn env에 LANG UTF-8 강제 (양 서비스 동일 적용).
검증: env -i(launchd 재현)에서 lineage/cwd 클레임 정상 복원, 유닛 30/30, tsc 통과.
idle 레인(fix/live-lane-idle-gjc-sessions)의 핵심 신호가 ps comm === 'gjc'인데,
macOS script install은 gjc가 런타임(comm=bun)으로 떠서 idle 레인 전체가 무력했다
(실측: 스폰 직후 세션이 목록에 영영 안 뜸).

- parseGjcPidsFromPsArgs 신설: ps -eo pid=,args= 의 argv에서 gjc 실행 파일 증거
  (argv0 basename 'gjc', 또는 '/' 포함 토큰의 basename 'gjc'/'gjc.js') → pid 집합.
  bare 'gjc' 단어(grep gjc 등)는 증거로 안 침. 셸 래퍼 꼬리 구두점(;) 정규화.
- findIdleGjcTmuxSessions·classifyExternalSessions에 gjcPids 주입 —
  comm 계약은 유지하고 argv 증거는 합성 'gjc' comm으로 흡수.

검증: providers 유닛 104/107(실패 3건은 본 브랜치 기존 gjc-synchronizer 테스트
격리 누수 — 실HOME 세션 스캔, 4!==1 — 본 변경과 무관), notify 7/7, tsc·eslint 통과.
실기(darwin) idle 행 3건 검출(스폰 직후 세션 포함), launchd 재현 env 동일.
@thisisjun786 thisisjun786 changed the title fix(server): idle 레인 + 라이브 감지 macOS 호환 — comm/lsof/proc/locale 리눅스 가정 제거 fix(server): macOS support for the idle gjc lane + live detection — remove comm/lsof/proc/locale Linux assumptions Jul 12, 2026
idle-gjc 행은 클릭 불가라 첫 메시지를 보내려면 tmux로 들어가야 했다 —
웹/모바일 전용 사용에선 스폰 후 막다른 길. lineage 등급 idle 행에 인라인
컴포저를 붙여 기존 /sessions/live/send 릴레이(관제탑 경유, 서버측 lineage
게이트 동일)로 첫 메시지를 보낸다. 전송되면 gjc가 transcript를 열고 5초
폴이 실제 대화 행으로 전환한다. 비-lineage 행에는 절대 노출 안 됨(patina
계약). 테스트 5/5.
idle 행의 기준은 '대화 없음'이 아니라 'transcript fd 미보유'다: 턴이 끝나
프롬프트에서 쉬는 세션은 대화가 있어도 idle 행으로 돌아온다(실측: 첫 메시지
응답 후에도 '아직 대화가 없습니다' 표시). 두 상태 모두에 정직한 문구로 교체.
…ne first-message composer

계획상 커밋 4(스레딩+진입점+테스트 noop)와 5(컴포저 제거+테스트 갱신)를 병합 —
executor 슬라이스가 최종 상태 트리를 산출해 중간 상태 재구성은 리스크만 추가.
내용은 계획 §2 Step 6·7, §3 그대로. 병합 근거는 ultragoal ledger에 기록.
- 대기/미로딩 행의 서브타이틀('프롬프트 대기 중 — …', '대화 미로딩 — …') 삭제,
  설명은 title 툴팁으로 이동 (반복 스캐폴딩 = UI 슬롭)
- 목록 하단 면책 문구('tmux 안에서 도는 gjc 세션만 감지…') 삭제
- 대기 뷰 본문 문구 압축, '이름 미확인 세션'→'이름 미확인'
tmux 이름이 없는 라이브 세션(Orca·일반 터미널에서 켠 gjc)은 웹에서 전송도
종료도 불가능한 노이즈 행('이름 미확인')이라 이 목록에서 제외. liveSessionIds
에는 그대로 남아 프로젝트 목록 LIVE 뱃지와 열람-전용 배너 보호는 유지된다.
이름 폴백 렌더링('이름 미확인'·제목 대체)은 죽은 코드가 되어 함께 회수.
- livePollBoost: 릴레이 전송 성공 시 30초간 live 폴링을 1초 간격으로 부스트
  (대기→라이브 전환 체감 지연 축소; 평시 5초 부하 불변). 순수 헬퍼 + 테스트 5건.
- useProjectsState: setInterval → 자기 스케줄링 setTimeout(동적 지연).
- SidebarSpawnSession: 관제탑 NAME_RE 미러 사전검증 — 이름에 / 등 불가 문자가
  있으면 요청 전에 한국어 규칙 안내, 관제탑의 영문 invalid-name 400도 동일 문구로 매핑.
- dir-suggestions에 scope=spawn 추가: TOWER_ALLOWED_ROOTS(관제탑과 동일 계약)의
  자식 폴더를 홈보다 먼저 제안, 중복 제거, 상한 20 유지. 반환 문자열은 tower
  resolveSpawnCwd(홈→허용루트)가 그대로 받는 상대 이름.
- 파일 패널은 기존 홈 전용 스코프 유지(home+제안 join 계약 보존).
- HomeDirInput scope prop: spawn 스코프는 빈 입력에서도 기본 목록 노출.
- 스폰 폼 placeholder 갱신. 서버 테스트 2건 추가(루트 우선/중복/traversal/누락 루트 강등).
리뷰 HIGH: tmuxId를 빼면 세대 비교가 통째로 건너뛰어져 같은 이름으로 재생성된
다른 세션에 전송/종료가 가능했다. 서버는 누락·비정형 토큰을 400으로 거부하고,
클라이언트(릴레이 컴포저·kill 버튼)는 토큰이 없으면 로컬에서 fail-closed로 거부한다.
리뷰 HIGH: 임의 위치의 /.../gjc 토큰(vim /tmp/gjc 등)이 lineage 승격 증거로
오인됐다 — argv[0]=gjc 또는 bun/node의 argv[1] 스크립트만 인정.
리뷰 MEDIUM: 전역 'lsof -c bun -c node'가 Node 많은 호스트에서 4MiB/4s 가드를
자초해 lane 전체를 떨어뜨릴 수 있었다 — ps 스냅샷 1회로 gjc pid+후손을 구한 뒤
'lsof -a -p <csv>'로 범위 한정. 음성/공백경로/순환 ppid 테스트 추가.
리뷰 MEDIUM 2건: 새 세대가 synthetic 행 없이 transcript 행으로 바로 나타나면
waiting view가 옛 세대에 영구 정체했다 — 같은 이름의 모든 lineage 행에서 다른
non-null 세대 관측 시 무효화. 15초 타임아웃 뒤에도 초당 프로젝트 재조회가
계속됐다 — 재조회만 중단, 복구 자동 이동은 유지(의도적 결정, 주석 명기).
리뷰 MEDIUM: bare 이름 제안은 홈에 같은 이름이 있으면 표시(작업공간 우선)와
실제 스폰 위치(홈 우선)가 어긋났다. 작업공간 항목은 절대경로(관제탑이 그대로
수용), 홈 항목은 상대 유지 — 고른 문자열이 곧 스폰 위치다. 절대경로 prefix
연속 타이핑 지원(+traversal 가드 테스트). LOW: 입력 클리어/실패 후 잔상 제안
무효화(seq 증가·목록 클리어).
인라인 정규식 3곳(스폰 규칙 1, 표시 안전성 2)을 shared/tmuxSessionName.ts로
통합 — 두 등급(생성용 strict / 표시용 loose)의 차이를 문서화. SpawnStatus의
도달 불가 'ok' variant 제거.
lsof pid 한정 커밋의 배선 버그: 'ps -eo pid=,ppid=,args=' 원문을 2열용
parseGjcPidsFromPsArgs에 넣어 ppid가 argv[0]으로 해석 → gjc 증거 0건 →
live/idle 행 전멸(배포 후 실측). 레코드 기반 gjcPidsFromProcessRecords로
교체하고, 정확히 이 배선을 고정하는 회귀 테스트 추가.
터미널 릴레이는 텍스트만 나를 수 있으므로, 이미지는 기존 자산 업로드
(POST /api/assets/images, 네이티브 컴포저와 동일 스토어)로 저장하고 절대경로를
메시지에 동봉한다 — pane의 gjc가 멀티모달 read 도구로 연다. 버튼+클립보드
붙여넣기, 최대 5장(엔드포인트 한도 미러), 업로드 실패 시 전송 자체를 취소.
전 항목 절대경로화는 드롭다운을 루트 접두어로 익사시켰다(실사용 피드백:
의미 부분이 truncate). 관제탑의 홈→루트 해석 순서를 미러하는 shadow 검사로,
홈에 같은 상대경로가 실존하는 항목만 절대경로로 구분하고 나머지는 bare 이름.
절대경로 대신 'Dev Workspace/argus' 별칭: 관제탑이 루트의 부모 기준으로
해석(관제탑에 별칭 후보 추가, 봉쇄 계약 동일). 별칭 prefix 연속 타이핑 지원.
일부만 별칭이고 나머지는 bare인 혼합 표기는 더 혼란스럽다는 피드백 반영.
워크스페이스 항목은 예외 없이 'Dev Workspace/<이름>' 형태(shadow 검사 제거로
단순화), 홈 항목은 상대 유지. 별칭 자체 타이핑('Dev W…')도 완성 단계로 제안.
가재코드의 대화형 선택지 도구명은 소문자 'ask'인데 렌더 레지스트리엔
Claude식 'AskUserQuestion'만 있어 Default(접힌 Parameters JSON)로 떨어졌다 —
tmux TUI가 메뉴에서 대기하는 동안 웹 transcript에선 질문·선택지가 사실상
안 보임(실사고). question-answer 렌더러로 등록(질문 첫 줄 제목·기본 펼침),
toolResult(터미널에서 고른 답)는 표시 유지. 테스트 3건.
관제탑 /answer: 메뉴를 라벨로 탐색하되 방향키 맹목 카운팅이 아니라 매 스텝
capture-pane으로 커서 행을 확인하고, 커서 라벨이 목표와 정확히 일치할 때만
Enter. 메뉴 부재/라벨 불일치/멀티셀렉트/도달 실패 시 Enter 없이 거부(409).
맨 위 고정(Up, 래핑 없음)→한 칸씩 하강으로 커서 시작 위치 의존성 제거.
서버 /sessions/live/answer는 send/kill과 동일한 lineage+세대토큰 게이트.
UI: LiveAnswerContext로 라이브 세션에서만 ask 카드 옵션을 클릭 버튼화(미답변
질문 한정), 단일 질문 자동 펼침, 지난 질문 클릭은 409로 fail-closed 표시.
테스트: parseMenuRows 4·classifyAnswerResponse·QuestionAnswer 라이브버튼 2.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant