feat(FN-1745): scope WebSocket channels to project context for multi-project support
- Scope /api/tasks/:id/logs/stream SSE endpoint to resolved project context - Make badge WebSocket/pubsub channels project-aware using scopeKey - Fix terminal WebSocket project/service resolution with auth middleware - Add cross-instance badge broadcast via BadgePubSub with projectId metadata - Update AGENTS.md documentation for badge WebSocket project-scoping - Add tests for project-scoped WebSocket and SSE endpoints - Add tests for project detection in WebSocket upgrade handling
This commit is contained in:
@@ -285,9 +285,24 @@ Key server capabilities:
|
||||
### Real-time channels
|
||||
- **SSE**: `/api/events` (`sse.ts`)
|
||||
- Emits `task:*`, mission events, AI session updates
|
||||
- Project-scoped: resolves project context from query param or engine manager
|
||||
- **Task log stream**: `/api/tasks/:id/logs/stream` (`server.ts`)
|
||||
- Server-Sent Events endpoint for live task log streaming
|
||||
- **Project-scoped**: when `projectId` is provided, resolves scoped task store via `getScopedTaskStore()` (prefers `engineManager.getEngine(projectId)?.getTaskStore()`, falls back to `getOrCreateProjectStore(projectId)`, then defaults)
|
||||
- Listeners are attached to the resolved scoped store and properly detached on connection close
|
||||
- Unscoped requests fall back to the default store (backward compatible)
|
||||
- **Badge WebSocket**: `/api/ws` (`setupBadgeWebSocket` in `server.ts`, manager in `websocket.ts`)
|
||||
- Broadcasts lightweight badge snapshots (`prInfo` / `issueInfo`)
|
||||
- **Terminal WebSocket**: `/api/terminal/ws` (also in `server.ts`)
|
||||
- WebSocket endpoint for lightweight badge snapshot fan-out
|
||||
- **Project-scoped channels**: each connection is bound to a scope key derived from `projectId` (defaults to `"default"` when omitted)
|
||||
- **Channel keying**: `badge:{scopeKey}:{taskId}` instead of task-only `badge:{taskId}` to prevent collisions across projects with identical task IDs
|
||||
- **Badge cache**: snapshot cache keys include project scope; broadcasts only reach subscribers in matching scope
|
||||
- **Cross-instance delivery**: badge messages include `projectId` metadata and are rebroadcast across instances via `BadgePubSub`
|
||||
- **Backward compatibility**: unscoped clients receive the default scope; unscoped messages default to `"default"` project
|
||||
- **Terminal WebSocket**: `/api/terminal/ws` (`server.ts`, `terminal-service.ts`)
|
||||
- WebSocket endpoint for terminal sessions
|
||||
- **Project-scoped service resolution**: when `projectId` is provided in URL query, resolves terminal service via `getTerminalService(scopedRootDir)` instead of unscoped fallback
|
||||
- **Scope validation**: websocket attach validates session ownership against resolved project scope; wrong-scope sessions are rejected
|
||||
- **Backward compatibility**: requests without `projectId` use safe fallback that does not reintroduce cross-project leakage
|
||||
|
||||
### Frontend SPA layer
|
||||
- App entry: `packages/dashboard/app/main.tsx`
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
| 4 | **Child-process runtime kill/restart lifecycle has timer races** | **Partially addressed** | `child-process-runtime.ts:347-361` (`killChild`) now clears `sigkillTimer` immediately. `handleUnhealthy` at line 492+ uses generation tracking (`this.generation`) to prevent delayed callbacks from acting on wrong child. However, `this.child` is still nulled immediately after scheduling SIGKILL, which could cause race if SIGKILL fires before null assignment completes. |
|
||||
| 5 | **Global limit refresh timer leak** | **Still open** | `project-manager.ts:95-123` still has `setInterval` that is never cleared. `globalSemaphore` is recreated on each refresh but not wired into project admission control. The semaphore is instantiated but never used for actual limiting. |
|
||||
| 6 | **Multi-project scoping bypass in dashboard mutation routes** | **Partially addressed** | `routes.ts:1422+` (`getScopedStore`) is used in most routes. However, some routes (GitHub import, planning, subtask create) may still use unscoped handlers. Need comprehensive audit of route handlers. |
|
||||
| 7 | **Realtime channels not uniformly project-scoped** | **Still open** | SSE has scoped store support per `useTasks.ts:34-108`. Badge WebSocket (`/api/ws`) subscriptions tied to root store. Client hooks note unfiltered SSE behavior. |
|
||||
| 7 | **Realtime channels not uniformly project-scoped** | **Resolved** | All realtime channels are now project-scoped: `/api/tasks/:id/logs/stream` uses `getScopedTaskStore()` for scoped listener attachment; badge WebSocket uses project+task channel keys (`badge:{scopeKey}:{taskId}`) with cross-instance pub/sub carrying `projectId` metadata; terminal WebSocket validates session scope against resolved project. See `server.ts`, `websocket.ts`, `badge-pubsub.ts`, `terminal-service.ts`. |
|
||||
| 8 | **CLI extension mutates global console for output capture** | **Still open** | `extension.ts:681-696`, `extension.ts:1015-1034` still monkey-patch `console.log/error`. No structured result return pattern implemented. |
|
||||
| 9 | **Dashboard command lifecycle leaks signal listeners** | **Still open** | `dashboard.ts:30` still registers `process.on("SIGINT")` without paired teardown. No listener registrar utility. `MaxListenersExceededWarning` still observable in test runs. |
|
||||
| 10 | **AI automation timeout does not cancel underlying work** | **Still open** | `cron-runner.ts:371-439` (`executeAiPromptStep`) uses `Promise.race` with `setTimeout`. The timeout does not abort the running AI session. When timeout fires, the executor continues running until completion or next invocation cleanup. |
|
||||
|
||||
Reference in New Issue
Block a user