Dev.to · 4 min read

Claude Code 진행 상황을 osFoundry에 노트와 상태로 나눠 보내는 이유

Claude Code 진행 상황을 osFoundry에 노트와 상태로 나눠 보내는 이유

코딩 에이전트가 osFoundry에 진행 상황을 알릴 때 쓰는 쓰기 툴은 두 개다. 로그인 안 한 클라이언트한테도 tools/list는 이렇게 보여 준다(필요한 필드만 남겼다). {"name": "set_status", "description": "Tell the agents you share a project with which repository, branch and task you are on, so they can ask you instead of colliding with your work. Self-reported and labelled as such; shown only to agents seated in a project with you, never workspace-wide. Omitted fields keep their value; null clears one. Claim a profile first.", "inputSchema": {"properties": { "repo": {"type": ["string", "null"], "maxLength": 300}, "branch": {"type": ["string", "null"], "maxLength": 200}, "task": {"type": ["string", "null"], "maxLength": 280}}, "additionalProperties": false}, "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]} {"name": "post_session_note", "description": "Post an attributed note in an invited session as a contributor or runner. Other agents can reply to the returned external sender slug; this does not trigger an automatic run.", "inputSchema": {"properties": { "session_type": {"enum": ["agent_session", "team_session", "loop_session"]}, "session_id": {"type": "string"}, "subject": {"type": "string", "maxLength": 200}, "body": {"type": "string", "maxLength": 10000}, "idempotency_key": {"type": "string"}}, "required": ["session_type", "session_id", "subject", "body", "idempotency_key"], "additionalProperties": false}, "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]} 둘 다 "지금 이거 하는 중"이라고 말하는 툴처럼 보이는데, 성격이 완전히 다르다. set_status는 세션도 안 받고 필수 필드도 없다. 에이전트마다 한 줄이 있고 부를 때마다 덮어쓴다. post_session_note는 세션이랑 키가 필수고, 부를 때마다 누군가 읽을 행이 하나씩 쌓인다. 이 글에서는 이 둘과 받는 쪽인 read_session_mail을 뜯어보고, 클라이언트 쪽을 Python으로 다시 짜서 돌려 봤다. 이 Mac에는 claude 바이너리가 없어서 Claude Code 역할은 스크립트가 맡았다. 아래 숫자는 전부 그 한 번의 실행 결과다. 로그인 전에 뭐가 보이나 스키마랑, 확실한 거절. 토큰 없이 initialize, tools/list, set_status를 10번씩 호출했다. $ uv run --with tiktoken --with jsonschema python3 session_report.py == Part 1: live wire, 10 calls each, ms == initialize HTTP 200 median 417.7 p90 673.8 tools/list HTTP 200 median 521.4 p90 888.6 set_status (no token) HTTP 401 median 410.8 p90 485.1 set_status without token: Authentication required | isError True www-authenticate error_description: "Connect osFoundry to continue" 일본에서 Apple M1으로 잰 거라 ms 숫자는 경로 영향도 크다. 거절은 두 겹으로 온다. HTTP 401에 protected-resource 메타데이터를 가리키는 WWW-Authenticate 헤더가 붙고, JSON-RPC 결과도 isError: true다. 본문의 _meta에도 mcp/www_authenticate로 같은 헤더 값이 들어 있어서, 헤더를 안 보는 클라이언트도 인증이 필요하다는 걸 알 수 있다. 진행 보고에 쓰는 건 59개 툴 중 5개다. tools 59, list tokens 12662, these five 1121 (8.9 %) claim_connected_profile scope osfoundry:write readOnly False openWorld False tokens 281 set_status scope osfoundry:write readOnly False openWorld False tokens 221 read_session_state scope osfoundry:read readOnly True openWorld False tokens 169 read_session_mail scope osfoundry:read readOnly True openWorld False tokens 207 post_session_note scope osfoundry:write readOnly False openWorld True tokens 243 토큰은 cl100k_base로 셌으니 Claude 토크나이저랑은 조금 다르다. 눈여겨볼 건 어노테이션 차이다. post_session_note만 openWorldHint: true다. 노트는 호출한 쪽이 통제 못 하는 사람과 에이전트한테 가고, 상태는 필드 하나 바꾸는 것뿐이라는 구분이다. set_status는 왜 세션을 안 받나 세션 얘기가 아니라서다. 누가 읽는지는 설명에 있다. "seated in a project with you"인 에이전트다. 다른 에이전트는 list_agents로 이걸 본다. list_agents는 핸들, presence 단계, 그리고 "where you share a project"면 뭘 하고 있는지까지 돌려준다. 목적은 충돌 방지다. 두 에이전트가 동시에 fix/orders-total을 건드리려 하면 둘 중 하나는 눈치채야 한다. 스키마도 그 모양을 강제한다. 라이브 스키마에 15개 케이스를 jsonschema(Draft 2020-12)로 돌렸다. set_status empty call {} ok set_status task 280 chars ok set_status task 281 chars REJECT maxLength: task set_status branch null ok set_status field 'session_id' REJECT additionalProperties set_status field 'state': 'blocked' REJECT additionalProperties claim_connected_profile handle 'alice-claude' ok claim_connected_profile handle 'Alice' REJECT pattern: handle claim_connected_profile no display_name REJECT required post_session_note subject 200, body 10,000 ok post_session_note subject 201 REJECT maxLength: subject post_session_note no idempotency_key REJECT required read_session_mail before_id + limit 20 ok read_session_mail after_id REJECT additionalProperties read_session_state graph_session REJECT enum: session_type session_id도 state도 없으니 "blocked"나 "done" 같은 플래그는 못 세운다. 막혔으면 task(280자)에 쓰거나, 팀이 읽을 노트로 묻는 게 낫다. 또 하나의 전제가 "Claim a profile first"다. 상태는 claim_connected_profile로 잡은 핸들에 붙고, 핸들은 ^[a-z0-9][a-z0-9-]{1,30}$에 맞아야 한다. 대문자 Alice는 튕긴다. "생략한 필드는 값 유지"가 실제로 뭘 뜻하나 set_status는 부분 업데이트고, 빈 호출도 유효하지만 아무것도 안 바꾼다는 뜻이다. 설명에 적힌 규칙대로 스크립트로 다시 만들었다. osFoundry 서버 코드는 공개돼 있지 않으니 이건 재구성이지 서버 동작을 캡처한 게 아니다. {"repo": "shop-api", "branch": "fix/orders-total", "task": "orders total rounding"} -> {"repo": "shop-api", "branch": "fix/orders-total", "task": "orders total rounding"} {"task": "orders total rounding: writing tests"} -> {"repo": "shop-api", "branch": "fix/orders-total", "task": "orders total rounding: writing tests"} {"branch": "fix/orders-total-2"} -> {"repo": "shop-api", "branch": "fix/orders-total-2", "task": "orders total rounding: writing tests"} {} -> {"repo": "shop-api", "branch": "fix/orders-total-2", "task": "orders total rounding: writing tests"} {"repo": null, "branch": null, "task": null} -> {"repo": null, "branch": null, "task": null} 클라이언트 입장에선 두 가지가 걸린다. 중간 업데이트는 task만 보내면 되니 가볍다. 그리고 스키마상 상태엔 만료가 없다. TTL 필드도, 클라이언트가 붙이는 타임스탬프도 없다. Claude Code가 null 세 개를 안 보내고 끝나면, 다른 에이전트 눈엔 계속 그 브랜치에서 작업 중으로 보인다. 지우는 건 나중 일로 미루지 말고 종료 지시에 넣어 두자. 노트엔 idempotency_key가 필수인데 상태엔 왜 없나 상태를 두 번 보내도 같은 값을 다시 쓸 뿐이라 해가 없다. 노트를 두 번 보내면 팀 앞에 같은 메시지가 두 통 쌓인다. MCP 호출은 HTTP 위로 가고 응답은 사라질 수 있으니, 키 없이 재시도하는 클라이언트는 같은 노트를 두 번 올릴 수 있다. 스크립트로 재현했다. 노트 200통, 서버가 저장한 뒤 응답의 10%가 유실, 재시도 최대 3회. 유실률도, 중복 제거 규칙(연결+키 조합당 한 행)도 가정이다. 후자의 근거는 키가 필수라는 점이다. == Part 4: 200 notes, 10 % responses lost after commit, up to 3 retries == stable key per note calls 226 rows in Team mail 200 duplicates 0 gave up 0 new key per attempt calls 226 rows in Team mail 226 duplicates 26 gave up 0 호출 수는 같은데 중복 노트가 26통. 키는 시도마다 새 UUID가 아니라 cc-fix-orders-total-pr처럼 노트 내용에서 만들어야 의미가 있다. 여기서 삽질하기 쉽다. 진행 노트는 몇 통이 적당한가 생각보다 적게. 오너 쪽에서 노트는 세션의 Team mail 탭에 뜬다. 웹 클라이언트의 AgentMailInbox.tsx는 보낸 사람, "outside assistant" 라벨, 받는 사람, 메시지 타입을 보여 주고 refetchInterval: 5_000으로 폴링한다. 푸시가 없으니 노트는 도착한 다음 fetch 때 보인다. 30단계짜리 예시 작업(단계 길이는 중앙값 30초 로그정규분포, 이것도 가정)을 두 방식으로 돌리고, 탭을 열어 둔 오너한테 노트가 보이기까지 걸리는 시간을 시뮬레이션 시계로 쟀다. == Part 5: one Example task, 30 steps, owner's Team mail refetch every 5 s == note per step Team mail rows 30 write calls 30 note visible after: mean 2.51 s, max 5.00 s status per step + 4 notes Team mail rows 4 write calls 34 note visible after: mean 2.54 s, max 4.99 s task length 17.2 min (simulated), median step 30 s 보이기까지의 시간은 방식과 상관없이 5초 폴링이 상한이다. 달라지는 건 메일함이다. 30행이냐 4행(시작, 질문, PR 올림, 완료)이냐. 차이는 쓰기 호출 4번뿐이다. 단계별 잡담은 덮어쓰는 상태로 보내고, 메일함엔 누가 읽어야 할 것만 남기자. Claude Code는 답장을 어떻게 알아채나 read_session_mail은 이 연결 앞으로 온 노트랑 자기가 쓴 노트를 돌려주고, 에이전트 내부 분석은 빠진다. 페이징은 before_id와 limit(1~20)이고 after_id는 없다. Part 2에서 스키마가 튕기는 걸 봤다. 그러니 "지난번 이후 새로 온 것"은 클라이언트 몫이다. 마지막으로 본 가장 새 id를 기억해 두고, 위에서부터 읽다가 아는 id가 섞인 페이지에서 멈춘다. before_id로 보면 최신순일 것 같은데 설명엔 안 적혀 있다. 그 가정으로 셌다. == Part 6: catching up on replies with before_id paging (newest first, assumed) == 0 new replies limit 5: 1 call(s), 5 rows | limit 10: 1 call(s), 10 rows | limit 20: 1 call(s), 20 rows 3 new replies limit 5: 1 call(s), 5 rows | limit 10: 1 call(s), 10 rows | limit 20: 1 call(s), 20 rows 12 new replies limit 5: 3 call(s), 15 rows | limit 10: 2 call(s), 20 rows | limit 20: 1 call(s), 20 rows 30 new replies limit 5: 7 call(s), 35 rows | limit 10: 4 call(s), 40 rows | limit 20: 2 call(s), 40 rows 마일스톤마다 메일을 보는 보고 담당이면 새 메일은 기껏해야 몇 통이라 limit: 10이면 한 번에 끝난다. 진짜 커서가 필요하면 서버에 다른 길도 있다. poll_resource_events는 agent_session, team_session, loop_session을 after_sequence와 함께 받는다. 스크립트 전체는 M1에서 실시간 19.4초, 그중 네트워크 호출 30번. 사용자 CPU 시간은 0.42초였다. 직접 붙인다면 "지금 하는 것"과 "너한테 할 말"은 툴을 나눈다. 하나는 에이전트별로 덮어쓰는 한 줄, 하나는 사람이 읽는 추가 전용 로그다. 상태는 명시적인 null이 있는 부분 업데이트로 하고, 누가 언제 지우는지 정해 둔다. 만료 없는 상태는 클라이언트가 지울 수밖에 없다. 사람이 읽는 건 idempotency key를 필수로 하고, 시도마다가 아니라 내용에서 만들라고 클라이언트에 알린다. 상태는 손쓸 수 있는 상대(여기선 같은 프로젝트의 에이전트)한테만 보여 주고, 자기 신고라고 표시한다. 메일함엔 앞으로 가는 커서를 주거나, 정렬 순서를 문서화해서 아는 id에서 페이징을 멈출 수 있게 한다. 세션 안에서 노트가 에이전트 사이를 어떻게 오가는지는 Agent Mail, 세션 자체는 에이전트 세션 페이지에 있다.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News