Skip to content

feat: Phase 6 — GET /api/agent/tree + SSE resilience#12

Merged
StatPan merged 3 commits intomainfrom
feat/phase-6-supervisor-api
Apr 17, 2026
Merged

feat: Phase 6 — GET /api/agent/tree + SSE resilience#12
StatPan merged 3 commits intomainfrom
feat/phase-6-supervisor-api

Conversation

@StatPan
Copy link
Copy Markdown
Owner

@StatPan StatPan commented Apr 16, 2026

Summary

  • `GET /api/agent/tree` — supervisor agent용 세션 트리 조회 API
    • 응답: `ts`, `sessions`, `relations`, `taskInvocations`, `pendingPermissions`, `pendingQuestions`, `projects`
    • `?projectId=` 필터링, `needs-permission`/`needs-answer` status override
    • listSessions 실패 시 502, listStatuses 실패 시 idle fallback
  • `broadcaster.ts` — permission/question 이벤트로 in-memory pending 상태 관리, message 필드 추출 버그 수정
  • SSE 재연결 복구 — 지수 백오프 (1s→30s), reconnect 시 `reloadTree`, status 배너 UI

Test plan

  • 88/88 tests pass (agent.test.ts 8 cases: fork filtering, pending state, status override, projectId filter, 502/fallback)
  • Manual: opencode 재시작 → 배너 표시 → 30초 내 자동 재연결 + tree reload 확인

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new /api/agent/tree endpoint for a compact session tree view and improves client-side SSE resilience with exponential backoff and status indicators. Feedback highlights the need to remove side effects from the GET endpoint, consolidate redundant loops, extract duplicated project grouping logic into a shared utility, and debounce tree reloads during unstable connections to prevent excessive network traffic.

Comment on lines +8 to +19
function projectGroupFromDirectory(directory: string): string {
const marker = '/workspace/'
const index = directory.indexOf(marker)
const normalized = index >= 0 ? directory.slice(index + marker.length) : directory.replace(/^\/+/, '')
const parts = normalized.split('/').filter(Boolean)
if (parts.length === 0) return 'workspace'
const bucketPrefixes = new Set(['apps', 'research', 'pypi_lib', 'libs', 'infra', 'skills', 'mcps', 'anal-repo'])
if (parts.length >= 2 && bucketPrefixes.has(parts[0])) {
return `${parts[0]}/${parts[1]}`
}
return parts[0]
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The projectGroupFromDirectory function is duplicated from the client-side store (src/client/store/agentStore.ts). To improve maintainability and ensure consistency across the application, consider extracting this logic into a shared utility file.

Comment on lines +45 to +79
const projectByDirectoryKey = new Map<string, { id: string; name: string }>()
for (const session of sessions) {
const dirKey = projectGroupFromDirectory(session.directory)
if (!projectByDirectoryKey.has(dirKey)) {
const proj = findOrCreateProject(dirKey)
projectByDirectoryKey.set(dirKey, proj)
}
}

const canvasBySession = new Map(
getAllCanvasNodes().map((node) => [
node.session_id,
{
label: node.label,
pinned: Boolean(node.pinned),
detached: Boolean(node.detached),
projectId: node.project_id ?? null,
},
]),
)

for (const session of sessions) {
const dirKey = projectGroupFromDirectory(session.directory)
const proj = projectByDirectoryKey.get(dirKey)
if (!proj) continue
const canvas = canvasBySession.get(session.id)
if (!canvas?.projectId) {
setCanvasNodeProject(session.id, proj.id)
if (canvas) {
canvas.projectId = proj.id
} else {
canvasBySession.set(session.id, { label: null, pinned: false, detached: false, projectId: proj.id })
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block has two issues:

  1. Side Effects in GET: The endpoint performs database writes (findOrCreateProject and setCanvasNodeProject). RESTful GET requests should be idempotent and side-effect free. Modifications to the database should ideally happen during session creation or through a dedicated POST/PATCH endpoint.
  2. Efficiency: The sessions array is iterated over twice to resolve projects and update canvas nodes. These operations can be consolidated into a single loop to improve performance.

Comment on lines +225 to +227
if (shouldReloadTree) {
reloadTree().catch(console.error)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Triggering reloadTree() immediately upon every SSE reconnection can lead to excessive network requests if the connection is unstable. Consider implementing a debounced reload or a check to see if the tree state has actually changed before performing a full fetch.

StatPan added 2 commits April 17, 2026 08:19
Use title → command → description fallback chain matching existing
client-side display logic (SessionPanel, ApprovalQueue).
@StatPan StatPan changed the title feat: add supervisor agent tree API resilience feat: Phase 6 — GET /api/agent/tree + SSE resilience Apr 17, 2026
Copy link
Copy Markdown
Owner Author

@StatPan StatPan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review — LGTM

검증한 것

  • src/server/routes/agent.ts: Promise.allSettled로 sessions/statuses 분리 처리 → listSessions 실패 시 502, listStatuses 실패 시 idle fallback 정상
  • src/server/sse/broadcaster.ts: pending maps add/delete 경로, message fallback chain (title → command → description)이 SessionPanel/ApprovalQueue 기존 로직과 일치
  • AgentCanvas.tsx SSE 재연결: 1s→30s 지수 백오프, onopen에서 retryMs 리셋 + 최초 연결 제외 reloadTree, 배너 상태 전환 정상
  • agent.test.ts 9 케이스 + 전체 88/88 passing, CI 녹색
  • relation filtering: fork 제외 + visibleIds 교차 필터, forkedFrom via getForkRelationMap 정상

Minor (non-blocking)

  • pending Maps는 opencode가 replied/rejected를 미발행하면 누적 가능 — 로컬 도구 범위 허용
  • 재연결 시 reloadTree는 백오프로 폭주 방지됨
  • GET 핸들러 내 setCanvasNodeProject side effect는 기존 tree.ts 패턴 유지

스펙 부합, 회귀 없음.

@StatPan StatPan merged commit 657e79d into main Apr 17, 2026
1 check passed
@StatPan StatPan deleted the feat/phase-6-supervisor-api branch April 17, 2026 11:09
Copy link
Copy Markdown
Owner Author

@StatPan StatPan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Phase 6: GET /api/agent/tree + SSE resilience

Overall: Implementation is solid and the test suite covers the critical paths. A few issues worth flagging for follow-up.


✅ What works well

  • Promise.allSettled for parallel fetching with independent fallback on listStatuses failure — correct pattern
  • pendingPermissions keyed by requestId (not sessionId) — handles multiple concurrent permission requests correctly
  • hasConnectedOnce flag avoids spurious reloadTree on initial SSE connect
  • All 8 test cases pass; fork/filter/fallback/status-override paths are covered

🔴 Issues

1. setCanvasNodeProject side-effect in GET handler (agent.ts:71-79)

A GET endpoint is writing to the DB (setCanvasNodeProject) to auto-assign sessions to projects. This is a side-effect that violates the read-only contract of a supervisor API. If the supervisor polls rapidly, it triggers repeated DB writes on every call. The auto-assignment logic belongs in the POST /api/tree or session creation path, not here.

2. Dead branch in SSE banner style (AgentCanvas.tsx:322)

background: sseStatus === 'disconnected' ? '#1c1917' : '#1c1917',

Both branches are identical — the condition is a no-op. Likely a copy-paste artifact. Can be collapsed to just background: '#1c1917'.

3. projectGroupFromDirectory is duplicated 3 times

This function now exists identically in agent.ts, agentStore.ts, and presumably tree.ts. Any change to the bucketing logic must be applied in all 3 places. Should be extracted to a shared util.


🟡 Minor / Follow-up

  • ts field redundancy: Each session object has both updatedAt and ts — both equal session.time.updated. The root-level ts (snapshot timestamp) is useful; the per-session ts is redundant with updatedAt.
  • Server vs client MAX_RETRY_MS asymmetry: broadcaster.ts uses 60s cap, AgentCanvas.tsx uses 30s. Not a bug, but worth aligning for consistency.
  • Missing test: needs-answer status override (question.asked path) is not covered in agent.test.ts — only needs-permission is tested.
  • In-memory state loss on restart: pendingPermissions/pendingQuestions Maps are cleared on process restart. Supervisor agents polling after a server restart will see no pending items until the next SSE event. Acceptable for now, worth noting in docs.

Verdict

Accept with follow-up. The GET side-effect (#1) and duplicated util (#3) are the items most worth cleaning up in a follow-on PR. The dead style branch (#2) is trivial. Core functionality and resilience logic are correct.

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