ai-agent-session-center 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +484 -429
  2. package/docs/3D/ADAPTATION_GUIDE.md +592 -0
  3. package/docs/3D/index.html +754 -0
  4. package/docs/AGENT_TEAM_TASKS.md +716 -0
  5. package/docs/CYBERDROME_V2_SPEC.md +531 -0
  6. package/docs/ERROR_185_ANALYSIS.md +263 -0
  7. package/docs/PLATFORM_FEATURES_PROMPT.md +296 -0
  8. package/docs/SESSION_DETAIL_FEATURES.md +98 -0
  9. package/docs/_3d_multimedia_features.md +1080 -0
  10. package/docs/_frontend_features.md +1057 -0
  11. package/docs/_server_features.md +1077 -0
  12. package/docs/session-duplication-fixes.md +271 -0
  13. package/docs/session-terminal-linkage.md +412 -0
  14. package/package.json +63 -5
  15. package/public/apple-touch-icon.svg +21 -0
  16. package/public/css/dashboard.css +0 -161
  17. package/public/css/detail-panel.css +25 -0
  18. package/public/css/layout.css +18 -1
  19. package/public/css/modals.css +0 -26
  20. package/public/css/settings.css +0 -150
  21. package/public/css/terminal.css +34 -0
  22. package/public/favicon.svg +18 -0
  23. package/public/index.html +6 -26
  24. package/public/js/alarmManager.js +0 -21
  25. package/public/js/app.js +21 -7
  26. package/public/js/detailPanel.js +63 -64
  27. package/public/js/historyPanel.js +61 -55
  28. package/public/js/quickActions.js +132 -48
  29. package/public/js/sessionCard.js +5 -20
  30. package/public/js/sessionControls.js +8 -0
  31. package/public/js/settingsManager.js +0 -142
  32. package/server/apiRouter.js +60 -15
  33. package/server/apiRouter.ts +774 -0
  34. package/server/approvalDetector.ts +94 -0
  35. package/server/authManager.ts +144 -0
  36. package/server/autoIdleManager.ts +110 -0
  37. package/server/config.ts +121 -0
  38. package/server/constants.ts +150 -0
  39. package/server/db.ts +475 -0
  40. package/server/hookInstaller.d.ts +3 -0
  41. package/server/hookProcessor.ts +108 -0
  42. package/server/hookRouter.ts +18 -0
  43. package/server/hookStats.ts +116 -0
  44. package/server/index.js +15 -1
  45. package/server/index.ts +230 -0
  46. package/server/logger.ts +75 -0
  47. package/server/mqReader.ts +349 -0
  48. package/server/portManager.ts +55 -0
  49. package/server/processMonitor.ts +239 -0
  50. package/server/serverConfig.ts +29 -0
  51. package/server/sessionMatcher.js +17 -6
  52. package/server/sessionMatcher.ts +403 -0
  53. package/server/sessionStore.js +109 -3
  54. package/server/sessionStore.ts +1145 -0
  55. package/server/sshManager.js +167 -24
  56. package/server/sshManager.ts +671 -0
  57. package/server/teamManager.ts +289 -0
  58. package/server/wsManager.ts +200 -0
@@ -0,0 +1,716 @@
1
+ # Agent Team Tasks — Scene Improvements Round 2
2
+
3
+ ## Task 1: Remove Ended Sessions from the 3D Map
4
+
5
+ ### Problem
6
+
7
+ When a session's status becomes `ended`, the robot stays on the map in an "offline" state — slumped over with dimming visor and core. The user wants ended sessions to simply disappear from the 3D scene entirely. The robot body glow during working/prompting is sufficient visual feedback; dead robots cluttering the map are unwanted.
8
+
9
+ ### Root Cause
10
+
11
+ In `src/components/3d/CyberdromeScene.tsx:84-101`, `SceneContent` iterates over ALL sessions from the store including ended ones:
12
+ ```tsx
13
+ const sessionArray = useMemo(() => [...sessions.values()], [sessions]);
14
+ // ↑ includes ended sessions → renders offline robots
15
+ ```
16
+
17
+ In `src/lib/robotStateMap.ts:31`, `ended` maps to `offline` robot state which renders a slumped, dimming robot with death animation. There's no filtering anywhere.
18
+
19
+ ### Fix Required
20
+
21
+ 1. **Filter out ended sessions** in `CyberdromeScene.tsx` `SceneContent`:
22
+ ```tsx
23
+ const sessionArray = useMemo(
24
+ () => [...sessions.values()].filter(s => s.status !== 'ended'),
25
+ [sessions],
26
+ );
27
+ ```
28
+
29
+ 2. **Clean up workstation occupancy** when a session ends. In `SessionRobot.tsx`, the existing unmount cleanup (line 287-294) already releases the desk on unmount. Since filtering removes ended robots, React will unmount those `SessionRobot` components and the cleanup effect fires automatically. **Verify this works** — if the robot was seated at a desk, the desk should become available after the session ends.
30
+
31
+ 3. **RobotListSidebar** (`src/components/3d/RobotListSidebar.tsx`): Also filter out ended sessions from the sidebar list:
32
+ ```tsx
33
+ const sessionArray = useMemo(() => {
34
+ const arr = [...sessions.values()].filter(s => s.status !== 'ended');
35
+ // ... sort logic
36
+ }, [sessions]);
37
+ ```
38
+
39
+ 4. **SceneOverlay** (`src/components/3d/SceneOverlay.tsx`): The status breakdown at top-left still shows ended counts. This is fine to keep (informational), but if desired, can also be filtered.
40
+
41
+ 5. **robotPositionStore cleanup**: When a session ends, the position should be cleaned up. The existing `useEffect` cleanup on unmount (line 418-422) already handles this since the component will unmount.
42
+
43
+ ### Files to Modify
44
+
45
+ | File | Change |
46
+ |------|--------|
47
+ | `src/components/3d/CyberdromeScene.tsx` | Filter `sessionArray` to exclude `ended` sessions |
48
+ | `src/components/3d/RobotListSidebar.tsx` | Filter `sessionArray` to exclude `ended` sessions |
49
+
50
+ ### Verification
51
+
52
+ 1. `npx tsc --noEmit` — type check
53
+ 2. `npx vitest run` — tests pass
54
+ 3. Visual: when a session ends, its robot disappears from the 3D scene within 1 frame
55
+ 4. Visual: ended sessions no longer appear in the right sidebar
56
+ 5. Verify the desk the ended robot occupied becomes available for other robots
57
+
58
+ ---
59
+
60
+ ## Task 2: Door-Aware Pathfinding for Robots
61
+
62
+ ### Problem
63
+
64
+ Robots currently navigate by walking in a straight line toward their target, bouncing off walls via `collidesAnyWall()`. When a robot inside a room wants to reach a destination outside (or vice versa), it gets stuck against walls because it doesn't know where the door is. It randomly picks a new wander direction when blocked, which is inefficient and unrealistic.
65
+
66
+ ### Current Navigation Logic (`SessionRobot.tsx:296-368`)
67
+
68
+ ```
69
+ 1. Compute direction to target (dx, dz)
70
+ 2. Rotate toward target
71
+ 3. Step forward
72
+ 4. If wall collision → try X-only, then Z-only, then pick new random target
73
+ ```
74
+
75
+ This brute-force approach means robots frequently get stuck against walls, especially when their target is on the other side of a wall.
76
+
77
+ ### Room Door Positions
78
+
79
+ Each room has exactly one door on the **south wall** (z = maxZ), centered at `(cx, 0, maxZ)` with `DOOR_GAP = 4` width. The door center is `(cx, 0, bounds.maxZ)` for each room.
80
+
81
+ ### Fix Required — Waypoint-Based Door Navigation
82
+
83
+ Instead of walking in a straight line, robots should plan a path through doors when their target is in a different zone.
84
+
85
+ #### A. Add door waypoint computation (`src/lib/cyberdromeScene.ts`)
86
+
87
+ Add a function to compute door waypoints:
88
+
89
+ ```ts
90
+ export interface DoorWaypoint {
91
+ roomIndex: number;
92
+ // Outside the door (south side, 1 unit past the wall)
93
+ outside: THREE.Vector3; // (cx, 0, bounds.maxZ + 1.0)
94
+ // Inside the door (north side, 1 unit inside the wall)
95
+ inside: THREE.Vector3; // (cx, 0, bounds.maxZ - 1.0)
96
+ }
97
+
98
+ export function buildDoorWaypoints(rooms: RoomConfig[]): DoorWaypoint[] {
99
+ return rooms.map(room => {
100
+ const [cx] = room.center;
101
+ const maxZ = room.bounds.maxZ;
102
+ return {
103
+ roomIndex: room.index,
104
+ outside: new THREE.Vector3(cx, 0, maxZ + 1.0),
105
+ inside: new THREE.Vector3(cx, 0, maxZ - 1.0),
106
+ };
107
+ });
108
+ }
109
+ ```
110
+
111
+ #### B. Add pathfinding helper (`src/lib/cyberdromeScene.ts`)
112
+
113
+ ```ts
114
+ /**
115
+ * Compute a waypoint path from current position to target.
116
+ * Returns an array of waypoints the robot should visit in order.
117
+ * - If same zone or corridor→corridor: direct path (empty waypoints)
118
+ * - If inside room → outside: [door.inside, door.outside, target]
119
+ * - If outside → inside room: [door.outside, door.inside, target]
120
+ * - If room A → room B: [doorA.inside, doorA.outside, doorB.outside, doorB.inside, target]
121
+ */
122
+ export function computePathWaypoints(
123
+ fromX: number,
124
+ fromZ: number,
125
+ target: THREE.Vector3,
126
+ fromZone: number, // getZone() result for current position
127
+ targetZone: number, // zone the target is in (-1 for corridor, -2 coffee, -3 gym, >=0 room)
128
+ doors: DoorWaypoint[],
129
+ ): THREE.Vector3[]
130
+ ```
131
+
132
+ The logic:
133
+ 1. If `fromZone === targetZone` → return `[target]` (direct path)
134
+ 2. If `fromZone >= 0` (inside a room) and `targetZone !== fromZone`:
135
+ - Get door for `fromZone` → add `door.inside`, `door.outside`
136
+ - If `targetZone >= 0` → also add destination room's `door.outside`, `door.inside`
137
+ - Add `target`
138
+ 3. If `fromZone < 0` (corridor/casual) and `targetZone >= 0`:
139
+ - Get door for `targetZone` → add `door.outside`, `door.inside`, `target`
140
+ 4. If both `< 0` → return `[target]`
141
+
142
+ #### C. Update navigation in `SessionRobot.tsx`
143
+
144
+ Add a waypoint queue to the `NavState`:
145
+
146
+ ```ts
147
+ interface NavState {
148
+ // ... existing fields ...
149
+ waypoints: THREE.Vector3[]; // NEW: queue of intermediate waypoints
150
+ waypointIdx: number; // NEW: current waypoint index
151
+ }
152
+ ```
153
+
154
+ Navigation update logic change:
155
+ - When setting `nav.target` for NAV_GOTO or NAV_WALK to a different zone, call `computePathWaypoints()` and store the result in `nav.waypoints`
156
+ - In `useFrame`, instead of walking directly to `nav.target`:
157
+ 1. Walk to `nav.waypoints[nav.waypointIdx]`
158
+ 2. When within 0.5 of current waypoint, advance to next: `nav.waypointIdx++`
159
+ 3. When all waypoints consumed, proceed to the original arrival logic (seat at desk, etc.)
160
+
161
+ #### D. Pass doors to SessionRobot
162
+
163
+ In `CyberdromeScene.tsx`, compute doors alongside workstations and pass to `SessionRobot`:
164
+ ```tsx
165
+ const doorWaypoints = useMemo(() => buildDoorWaypoints(roomConfigs), [roomConfigs]);
166
+ ```
167
+
168
+ Add `doors: DoorWaypoint[]` to `SessionRobotProps`.
169
+
170
+ ### Files to Modify
171
+
172
+ | File | Change |
173
+ |------|--------|
174
+ | `src/lib/cyberdromeScene.ts` | Add `DoorWaypoint` interface, `buildDoorWaypoints()`, `computePathWaypoints()` |
175
+ | `src/components/3d/SessionRobot.tsx` | Add `waypoints`/`waypointIdx` to `NavState`, update navigation in `useFrame` to follow waypoints, call `computePathWaypoints()` when setting targets |
176
+ | `src/components/3d/CyberdromeScene.tsx` | Compute `doorWaypoints`, pass as prop to `SessionRobot`, update `SceneContent` props |
177
+
178
+ ### Verification
179
+
180
+ 1. `npx tsc --noEmit`
181
+ 2. `npx vitest run`
182
+ 3. Visual: robot inside Room 0 gets assigned to Room 2 → walks to Room 0 door → exits → walks to Room 2 door → enters → sits at desk
183
+ 4. Visual: unassigned robot in corridor transitions to working → walks to a room door → enters → sits at desk
184
+ 5. Visual: robot leaving a room (idle→coffee) walks to door → exits → walks to coffee area
185
+ 6. No robots getting permanently stuck against walls
186
+
187
+ ---
188
+
189
+ ## Task 3: Fix React Error #185 (Maximum Update Depth + WebGL Context Lost)
190
+
191
+ ### Problem
192
+
193
+ When clicking a robot in the 3D scene, the app crashes with:
194
+ ```
195
+ Uncaught Error: Minified React error #185 (Maximum update depth exceeded)
196
+ ```
197
+ Followed by: `THREE.WebGLRenderer: Context Lost.`
198
+
199
+ React Error #185 means a component is calling `setState` during rendering in an infinite loop. The WebGL context loss is a consequence — the browser kills the GPU context when the main thread is frozen by the infinite loop.
200
+
201
+ ### Root Cause Analysis
202
+
203
+ The existing code has multiple mitigations (memo on SessionRobot/RobotLabel, startTransition for selection, hardcoded `isSelected=false`, seatedRef) but the error still occurs. The remaining culprit is the **drei `<Html>` portal cascade**:
204
+
205
+ 1. **RobotLabel** uses `<Html>` (drei) which creates a React portal from WebGL into DOM
206
+ 2. **RobotDialogue** also uses `<Html>` — another portal per robot
207
+ 3. When the user clicks a robot, `selectSession()` is called. Even though `SessionRobot` doesn't subscribe to `selectedSessionId`, **other components** (like `SceneOverlay`, `RobotListSidebar`, or the `DetailPanel` outside the Canvas) re-render. This can cause the R3F reconciler to flush, triggering `<Html>` portal reconciliation across all robots simultaneously.
208
+ 4. Each `<Html>` portal update can trigger more state updates (drei's internal resize observer, portal container management), creating a cascade.
209
+
210
+ Additionally, `RobotDialogue` has `useEffect` with `visible`/`fadingOut` state that may chain-update:
211
+ - `text` prop changes → `setVisible(true)` → re-render → `useEffect` fires again if `visible` was already truthy in the deps closure
212
+
213
+ ### Fix Required
214
+
215
+ #### A. Replace `<Html>` in RobotDialogue with 3D `<Text>` (`src/components/3d/RobotDialogue.tsx`)
216
+
217
+ The dialogue bubble is the most volatile component (updates on every tool call). Replace its `<Html>` portal with drei `<Text>` (troika SDF text rendered in WebGL), eliminating one portal per robot:
218
+
219
+ ```tsx
220
+ // Instead of <Html> portal:
221
+ <Billboard position={[0, 2.8, 0]} follow>
222
+ <Text fontSize={0.12} color="#fff" anchorX="center" anchorY="bottom" ...>
223
+ {displayText}
224
+ </Text>
225
+ </Billboard>
226
+ ```
227
+
228
+ This removes the DOM portal entirely. The visual result is similar — a floating text above the robot — but rendered natively in WebGL with zero DOM reconciliation overhead.
229
+
230
+ #### B. Stabilize RobotLabel memo (`src/components/3d/RobotLabel.tsx`)
231
+
232
+ The current memo comparison doesn't check `session.toolLog` length but `RobotLabelInner` reads `session.toolLog?.length` for the "N tool calls" display. Add it:
233
+
234
+ ```tsx
235
+ const RobotLabel = memo(RobotLabelInner, (prev, next) =>
236
+ prev.session.sessionId === next.session.sessionId &&
237
+ prev.session.status === next.session.status &&
238
+ prev.session.title === next.session.title &&
239
+ prev.session.projectName === next.session.projectName &&
240
+ prev.session.label === next.session.label &&
241
+ prev.session.currentPrompt === next.session.currentPrompt &&
242
+ prev.session.model === next.session.model &&
243
+ prev.session.startedAt === next.session.startedAt &&
244
+ (prev.session.toolLog?.length ?? 0) === (next.session.toolLog?.length ?? 0) &&
245
+ prev.robotState === next.robotState &&
246
+ prev.isSelected === next.isSelected &&
247
+ prev.isHovered === next.isHovered
248
+ );
249
+ ```
250
+
251
+ #### C. Throttle dialogue state updates (`src/components/3d/SessionRobot.tsx`)
252
+
253
+ The `useEffect` that sets `dialogue` fires on every `session.toolLog` reference change. If the store updates toolLog frequently (e.g., rapid tool calls), this causes rapid re-renders. Add a throttle:
254
+
255
+ ```tsx
256
+ // Only update dialogue if enough time has passed since the last update
257
+ const lastDialogueUpdate = useRef(0);
258
+ // In the useEffect:
259
+ const now = Date.now();
260
+ if (now - lastDialogueUpdate.current < 500) return; // throttle to 2Hz
261
+ lastDialogueUpdate.current = now;
262
+ ```
263
+
264
+ #### D. Verify `SessionRobot` memo stability
265
+
266
+ The memo comparison includes `prev.session === next.session`. Since the store creates a new `Map` on every update, and the session object may be a new reference even when nothing changed for this particular robot, this could cause unnecessary re-renders.
267
+
268
+ Consider making the comparison more granular — compare `session.sessionId`, `session.status`, `session.toolLog`, etc. instead of `session` reference equality.
269
+
270
+ ### Files to Modify
271
+
272
+ | File | Change |
273
+ |------|--------|
274
+ | `src/components/3d/RobotDialogue.tsx` | Replace `<Html>` portal with `<Billboard><Text>` (pure WebGL) |
275
+ | `src/components/3d/RobotLabel.tsx` | Add `toolLog.length` and `startedAt` to memo comparison |
276
+ | `src/components/3d/SessionRobot.tsx` | Throttle dialogue updates, improve memo comparison to avoid session reference equality |
277
+
278
+ ### Verification
279
+
280
+ 1. `npx tsc --noEmit`
281
+ 2. `npx vitest run`
282
+ 3. **Critical test**: Click a robot in the 3D scene — no Error #185, no WebGL context loss
283
+ 4. Click multiple robots rapidly — stable
284
+ 5. With 10+ robots active, click any robot — no crash
285
+ 6. Dialogue bubbles still appear above robots for tool calls and status changes
286
+ 7. Rebuild production: `npx vite build` — open in browser, reproduce the click test
287
+
288
+ ---
289
+
290
+ ## Task 4: Persist Robot Positions Across Page Refreshes
291
+
292
+ ### Problem
293
+
294
+ When the user refreshes the browser, all robots spawn at random positions near the origin and must re-navigate to their desks/casual areas. The user wants robots to remember their position and status so that after a refresh, the 3D scene looks the same as before — robots that were seated stay seated, robots that were walking continue from where they left off.
295
+
296
+ ### Current State
297
+
298
+ - `robotPositionStore` (`src/components/3d/robotPositionStore.ts`) is an in-memory `Map<string, {x, y, z}>` — lost on refresh
299
+ - `NavState` in `SessionRobot.tsx:126-138` initializes with random positions:
300
+ ```ts
301
+ posX: (Math.random() - 0.5) * 4,
302
+ posY: 0,
303
+ posZ: (Math.random() - 0.5) * 4,
304
+ ```
305
+ - Workstation `occupantId` fields are in-memory only — lost on refresh
306
+
307
+ ### Fix Required
308
+
309
+ #### A. Create a persistent position store (`src/lib/robotPositionPersist.ts`)
310
+
311
+ ```ts
312
+ interface PersistedRobotState {
313
+ posX: number;
314
+ posY: number;
315
+ posZ: number;
316
+ rotY: number;
317
+ mode: number; // NAV_WALK, NAV_GOTO, NAV_SIT, NAV_IDLE
318
+ deskIdx: number; // which workstation the robot occupies (-1 if none)
319
+ robotState: string; // 'idle' | 'working' | etc
320
+ }
321
+
322
+ const STORAGE_KEY = 'cyberdrome-robot-positions';
323
+
324
+ export function saveRobotPositions(data: Map<string, PersistedRobotState>): void {
325
+ // Convert to JSON and write to sessionStorage (survives refresh, not tab close)
326
+ const obj: Record<string, PersistedRobotState> = {};
327
+ data.forEach((v, k) => { obj[k] = v; });
328
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
329
+ }
330
+
331
+ export function loadRobotPositions(): Map<string, PersistedRobotState> {
332
+ const raw = sessionStorage.getItem(STORAGE_KEY);
333
+ if (!raw) return new Map();
334
+ const obj = JSON.parse(raw) as Record<string, PersistedRobotState>;
335
+ return new Map(Object.entries(obj));
336
+ }
337
+
338
+ export function clearRobotPositions(): void {
339
+ sessionStorage.removeItem(STORAGE_KEY);
340
+ }
341
+ ```
342
+
343
+ #### B. Periodically save positions (`src/components/3d/CyberdromeScene.tsx`)
344
+
345
+ Add a `useFrame` hook in `SceneContent` that throttles saves to every 2 seconds:
346
+
347
+ ```tsx
348
+ const lastSave = useRef(0);
349
+ useFrame(() => {
350
+ const now = performance.now();
351
+ if (now - lastSave.current < 2000) return;
352
+ lastSave.current = now;
353
+ // Collect all NavState from SessionRobot refs → saveRobotPositions()
354
+ });
355
+ ```
356
+
357
+ Alternatively, expose nav state from SessionRobot via a shared non-reactive store (similar to `robotPositionStore`) and have the save logic in `CyberdromeScene`.
358
+
359
+ #### C. Initialize SessionRobot from persisted state (`src/components/3d/SessionRobot.tsx`)
360
+
361
+ On mount, check if there's a persisted position for this session:
362
+
363
+ ```tsx
364
+ const persisted = loadRobotPositions().get(session.sessionId);
365
+ const nav = useRef<NavState>({
366
+ mode: persisted ? persisted.mode : NAV_WALK,
367
+ target: new THREE.Vector3(),
368
+ deskIdx: persisted ? persisted.deskIdx : -1,
369
+ speed: 1.0 + Math.random() * 0.7,
370
+ walkHz: 7 + Math.random() * 2,
371
+ phase: Math.random() * Math.PI * 2,
372
+ decisionTimer: 2 + Math.random() * 4,
373
+ posX: persisted ? persisted.posX : (Math.random() - 0.5) * 4,
374
+ posY: persisted ? persisted.posY : 0,
375
+ posZ: persisted ? persisted.posZ : (Math.random() - 0.5) * 4,
376
+ rotY: persisted ? persisted.rotY : Math.random() * Math.PI * 2,
377
+ });
378
+ ```
379
+
380
+ If `persisted.deskIdx >= 0`, also restore `workstations[persisted.deskIdx].occupantId = session.sessionId` to reclaim the desk.
381
+
382
+ #### D. Extend `robotPositionStore` to also store nav state
383
+
384
+ Add a parallel `navStateStore` (non-reactive Map) that SessionRobot writes to every frame (or throttled). The save logic reads from this store.
385
+
386
+ ### Files to Create/Modify
387
+
388
+ | File | Change |
389
+ |------|--------|
390
+ | `src/lib/robotPositionPersist.ts` | **NEW** — sessionStorage persistence for robot positions + nav state |
391
+ | `src/components/3d/SessionRobot.tsx` | Initialize `NavState` from persisted data; reclaim workstation desk; write nav state to shared store |
392
+ | `src/components/3d/robotPositionStore.ts` | Extend to also store nav mode, deskIdx, rotY |
393
+ | `src/components/3d/CyberdromeScene.tsx` | Add periodic save (every 2s) collecting all robot nav states |
394
+
395
+ ### Verification
396
+
397
+ 1. `npx tsc --noEmit`
398
+ 2. `npx vitest run`
399
+ 3. Open the 3D scene with multiple active sessions → robots navigate to desks and sit
400
+ 4. Refresh the page (F5) → robots appear at the same positions, seated robots are still seated
401
+ 5. Open DevTools → Application → Session Storage → verify `cyberdrome-robot-positions` key exists
402
+ 6. Close tab and reopen → robots start fresh (sessionStorage only survives refresh, not close)
403
+
404
+ ---
405
+
406
+ ## Task 5: Relocate Casual Areas to North Side + Enhance Content
407
+
408
+ ### Problem
409
+
410
+ The casual areas (Coffee Lounge & Gym) are currently placed **south** of the room grid (below the common area). The user wants them on the **north side** (behind the rooms, on the opposite side of the map). Additionally:
411
+ - Casual areas need visible **name labels** (like rooms have)
412
+ - Gym needs **at least 10 exercise devices** (currently only 4)
413
+ - Coffee area needs **coffee cups, a coffee pot, and a coffee machine** on the counter
414
+
415
+ ### Current Placement (`src/lib/cyberdromeScene.ts:367-438`)
416
+
417
+ ```
418
+ ↑ North (z decreases)
419
+ [Coffee] [Gym] ← User wants here (north of rooms)
420
+
421
+ [Room 0] [Room 1] [Room 2] [Room 3] ← Dynamic rooms
422
+ [Room 4] [Room 5] ...
423
+
424
+ [Common Area - 10 desks] ← Corridor workstations
425
+
426
+ [Coffee Lounge] [Gym] ← Currently here (south)
427
+ ```
428
+
429
+ ### Fix Required
430
+
431
+ #### A. Relocate casual areas to north of rooms (`src/lib/cyberdromeScene.ts`)
432
+
433
+ In `buildCasualAreas()`, change `baseZ` computation:
434
+
435
+ ```ts
436
+ // BEFORE: south of common area
437
+ // baseZ = southmostCenter[2] + ROOM_HALF + ROOM_GAP + 5 + 8 + CASUAL_HALF + 2;
438
+
439
+ // AFTER: north of all rooms
440
+ const minRow = 0; // rooms start at row 0
441
+ const northmostCenter = computeRoomCenter(0); // row 0 is always the northernmost
442
+ baseZ = northmostCenter[2] - ROOM_HALF - ROOM_GAP - CASUAL_HALF;
443
+ // This places them above (north of) the first row of rooms
444
+ ```
445
+
446
+ Also update `computeSceneBounds()` to account for the northward casual areas (add bounds check for negative Z values).
447
+
448
+ #### B. Add area name labels (`src/components/3d/RoomLabels.tsx` or new component)
449
+
450
+ Add labels for the casual areas similar to room labels. Either:
451
+ - Extend `RoomLabels` to also accept `CasualArea[]` and render labels
452
+ - Or create a small `CasualAreaLabels` component
453
+
454
+ Each casual area gets a ground-level 3D text label:
455
+ - Coffee: "COFFEE LOUNGE" in warm amber
456
+ - Gym: "FITNESS CENTER" in cool green/blue
457
+
458
+ Pass `casualAreas` from `CyberdromeScene.tsx` to the label component.
459
+
460
+ #### C. Expand gym to 10+ exercise devices (`src/components/3d/CyberdromeEnvironment.tsx`)
461
+
462
+ Current gym has 4 stations (bench press, treadmill, punching bag, weight rack). Expand to at least 10:
463
+
464
+ 1. Bench press (existing)
465
+ 2. Treadmill (existing)
466
+ 3. Punching bag (existing)
467
+ 4. Weight rack (existing)
468
+ 5. **Rowing machine** — flat angled platform with handle bar
469
+ 6. **Pull-up bar** — tall frame with horizontal bar at top
470
+ 7. **Kettlebell station** — spheres on floor mat
471
+ 8. **Stationary bike** — seat + handlebars + wheel
472
+ 9. **Cable machine** — tall frame with pulley
473
+ 10. **Battle ropes** — two cylinders (snaking lines) anchored to a post
474
+
475
+ Each device needs a station position in `buildCasualAreas()` for robot navigation. Expand the gym area size from 10 to ~16 to fit 10 stations in a grid layout:
476
+
477
+ ```ts
478
+ // Expand gym size
479
+ const GYM_SIZE = 16; // was using CASUAL_AREA_SIZE=10
480
+
481
+ // 10 stations in a 2×5 grid
482
+ const gymPositions = [];
483
+ for (let row = 0; row < 2; row++) {
484
+ for (let col = 0; col < 5; col++) {
485
+ gymPositions.push({
486
+ x: gymX - 6 + col * 3,
487
+ z: baseZ - 3 + row * 6,
488
+ rot: row === 0 ? 0 : Math.PI,
489
+ });
490
+ }
491
+ }
492
+ ```
493
+
494
+ #### D. Add coffee accessories to Coffee Lounge (`src/components/3d/CyberdromeEnvironment.tsx`)
495
+
496
+ Add to the existing `CoffeeLounge` component:
497
+
498
+ 1. **Coffee machine** — on the counter: box with a cylinder spout + small panel
499
+ ```tsx
500
+ <group position={[cx - 2, 1.15, cz - 4.5]}>
501
+ <mesh material={equipMat}><boxGeometry args={[0.4, 0.5, 0.35]} /></mesh>
502
+ <mesh position={[0.1, 0.15, 0.2]} material={accentMat}>
503
+ <cylinderGeometry args={[0.03, 0.03, 0.15, 6]} />
504
+ </mesh>
505
+ </group>
506
+ ```
507
+
508
+ 2. **Coffee pot** — on the counter: cylinder with handle
509
+ ```tsx
510
+ <group position={[cx + 1, 1.15, cz - 4.5]}>
511
+ <mesh material={accentMat}><cylinderGeometry args={[0.08, 0.06, 0.2, 8]} /></mesh>
512
+ <mesh position={[0.1, 0.05, 0]} material={equipMat}>
513
+ <torusGeometry args={[0.06, 0.01, 4, 8, Math.PI]} />
514
+ </mesh>
515
+ </group>
516
+ ```
517
+
518
+ 3. **Coffee cups** — on each table: 2 small cylinders per table
519
+ ```tsx
520
+ <mesh position={[t.x - 0.15, 0.54, t.z + 0.1]} material={accentMat}>
521
+ <cylinderGeometry args={[0.03, 0.025, 0.06, 6]} />
522
+ </mesh>
523
+ <mesh position={[t.x + 0.15, 0.54, t.z - 0.1]} material={accentMat}>
524
+ <cylinderGeometry args={[0.03, 0.025, 0.06, 6]} />
525
+ </mesh>
526
+ ```
527
+
528
+ #### E. Update scene bounds for northward placement
529
+
530
+ In `computeSceneBounds()`, add check for negative Z (northward areas):
531
+ ```ts
532
+ // Also check for casual areas north of rooms
533
+ if (rooms.length > 0) {
534
+ const northCenter = computeRoomCenter(0);
535
+ maxDist = Math.max(maxDist, Math.abs(northCenter[2] - ROOM_HALF - ROOM_GAP - CASUAL_HALF - 5));
536
+ }
537
+ ```
538
+
539
+ ### Files to Modify
540
+
541
+ | File | Change |
542
+ |------|--------|
543
+ | `src/lib/cyberdromeScene.ts` | Relocate `buildCasualAreas()` baseZ to north of rooms; expand gym to 10 stations; increase gym area size; update `computeSceneBounds()` for northward bounds |
544
+ | `src/components/3d/CyberdromeEnvironment.tsx` | Add 6 new gym devices to `GymArea`; add coffee machine, pot, cups to `CoffeeLounge`; adjust area floor size for expanded gym |
545
+ | `src/components/3d/RoomLabels.tsx` | Add casual area labels (or create `CasualAreaLabels` component in same file); accept `CasualArea[]` prop |
546
+ | `src/components/3d/CyberdromeScene.tsx` | Pass `casualAreas` to `RoomLabels` or new label component |
547
+
548
+ ### Verification
549
+
550
+ 1. `npx tsc --noEmit`
551
+ 2. `npx vitest run`
552
+ 3. `npx vite build`
553
+ 4. Visual: casual areas appear **north** of the room grid (above rooms in the scene)
554
+ 5. Visual: "COFFEE LOUNGE" and "FITNESS CENTER" labels on the ground near each area
555
+ 6. Visual: gym has 10 distinct exercise devices
556
+ 7. Visual: coffee counter has a coffee machine, pot, and cups on tables
557
+ 8. Idle robots still navigate to coffee area; waiting robots still navigate to gym
558
+ 9. Casual areas adapt colors when switching themes
559
+
560
+ ---
561
+
562
+ ## Task 6: Increase Robot Moving Speed Significantly
563
+
564
+ ### Problem
565
+
566
+ Robots move too slowly across the map. When navigating between rooms, to casual areas, or to desks, the walking animation feels sluggish. The user wants robots to move **much faster** so transitions feel snappy and the scene feels alive.
567
+
568
+ ### Current Speed Values
569
+
570
+ **Base speed** (`src/components/3d/SessionRobot.tsx:130`):
571
+ ```ts
572
+ speed: 1.0 + Math.random() * 0.7, // range: 1.0–1.7 units/sec
573
+ ```
574
+
575
+ **Speed multipliers per state** (`src/lib/robotStateMap.ts:58-123`):
576
+ | State | `speedMultiplier` | Effective speed |
577
+ |-------|-------------------|-----------------|
578
+ | idle | 0.6 | 0.6–1.0 |
579
+ | thinking | 0.7 | 0.7–1.2 |
580
+ | working | 1.0 | 1.0–1.7 |
581
+ | waiting | 0.3 | 0.3–0.5 |
582
+ | alert | 0 | frozen |
583
+ | input | 0 | frozen |
584
+ | offline | 0 | frozen |
585
+ | connecting | 0 | frozen |
586
+
587
+ **Movement step** (`SessionRobot.tsx:325`):
588
+ ```ts
589
+ const step = n.speed * behavior.speedMultiplier * dt;
590
+ ```
591
+
592
+ **Turn rate** (`SessionRobot.tsx:322`):
593
+ ```ts
594
+ n.rotY += diff * Math.min(1, 5 * dt); // rotation smoothing factor: 5
595
+ ```
596
+
597
+ ### Fix Required
598
+
599
+ #### A. Increase base speed (`src/components/3d/SessionRobot.tsx`)
600
+
601
+ Triple the base speed range:
602
+ ```ts
603
+ // BEFORE
604
+ speed: 1.0 + Math.random() * 0.7, // 1.0–1.7
605
+
606
+ // AFTER
607
+ speed: 3.0 + Math.random() * 1.5, // 3.0–4.5
608
+ ```
609
+
610
+ #### B. Increase state speed multipliers (`src/lib/robotStateMap.ts`)
611
+
612
+ Bump multipliers so robots are never crawling:
613
+ | State | Before | After |
614
+ |-------|--------|-------|
615
+ | idle | 0.6 | 1.0 |
616
+ | thinking | 0.7 | 1.2 |
617
+ | working | 1.0 | 1.5 |
618
+ | waiting | 0.3 | 0.8 |
619
+ | alert | 0 | 0 (stay frozen) |
620
+ | input | 0 | 0 (stay frozen) |
621
+ | offline | 0 | 0 (stay frozen) |
622
+ | connecting | 0 | 0 (stay frozen) |
623
+
624
+ #### C. Increase turn rate (`src/components/3d/SessionRobot.tsx`)
625
+
626
+ Faster movement needs faster turning or robots overshoot corners:
627
+ ```ts
628
+ // BEFORE
629
+ n.rotY += diff * Math.min(1, 5 * dt);
630
+
631
+ // AFTER
632
+ n.rotY += diff * Math.min(1, 10 * dt);
633
+ ```
634
+
635
+ #### D. Increase walk animation frequency (`src/components/3d/SessionRobot.tsx`)
636
+
637
+ The walk bounce should match the faster speed so the animation doesn't look floaty:
638
+ ```ts
639
+ // BEFORE
640
+ walkHz: 7 + Math.random() * 2, // 7–9 Hz bounce
641
+
642
+ // AFTER
643
+ walkHz: 12 + Math.random() * 4, // 12–16 Hz bounce
644
+ ```
645
+
646
+ ### Files to Modify
647
+
648
+ | File | Change |
649
+ |------|--------|
650
+ | `src/components/3d/SessionRobot.tsx` | Increase `speed` (3.0–4.5), `walkHz` (12–16), turn rate (5→10) |
651
+ | `src/lib/robotStateMap.ts` | Increase `speedMultiplier` for idle (1.0), thinking (1.2), working (1.5), waiting (0.8) |
652
+
653
+ ### Verification
654
+
655
+ 1. `npx tsc --noEmit`
656
+ 2. `npx vitest run`
657
+ 3. Visual: robots move noticeably faster when walking between locations
658
+ 4. Visual: robots don't overshoot targets or jitter (turn rate matches speed)
659
+ 5. Visual: walk bounce animation looks natural at the faster speed
660
+
661
+ ---
662
+
663
+ ## Execution Order & Parallelization Strategy
664
+
665
+ ```
666
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
667
+ │ Task 1 │ │ Task 3 │ │ Task 6 │ ← Parallel (independent)
668
+ │ Remove ended │ │ Fix Error #185 │ │ Increase speed │
669
+ │ sessions │ │ Html → Text │ │ (robotStateMap) │
670
+ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘
671
+ │ │ │
672
+ ▼ ▼ ▼
673
+ ┌─────────────────┐ ┌─────────────────┐
674
+ │ Task 2 │ │ Task 4 │ ← Sequential (shared SessionRobot.tsx)
675
+ │ Door pathfind │ │ Position persist│
676
+ │ (SessionRobot) │ │ (SessionRobot) │
677
+ └────────┬────────┘ └────────┬────────┘
678
+ │ │
679
+ ▼ ▼
680
+ ┌─────────────────────────┐
681
+ │ Task 5 │ ← Sequential (touches shared layout files)
682
+ │ Relocate casual areas │
683
+ │ + content enhancement │
684
+ └─────────────────────────┘
685
+ ```
686
+
687
+ **Phase 1** (parallel): Task 1 + Task 3 + Task 6
688
+ - Task 1 modifies `CyberdromeScene.tsx` (filter) + `RobotListSidebar.tsx`
689
+ - Task 3 modifies `RobotDialogue.tsx` + `RobotLabel.tsx` + `SessionRobot.tsx` (memo/throttle only)
690
+ - Task 6 modifies `robotStateMap.ts` (speed multipliers) + `SessionRobot.tsx` (base speed, walkHz, turn rate)
691
+ - **Note**: Task 3 and Task 6 both touch `SessionRobot.tsx` but in different areas (memo/dialogue vs speed init). Run Task 6 after Task 3 if conflicts arise.
692
+
693
+ **Phase 2** (sequential): Task 2 → Task 4
694
+ - Task 2 modifies `cyberdromeScene.ts` (add pathfinding) + `SessionRobot.tsx` (navigation logic) + `CyberdromeScene.tsx` (pass doors)
695
+ - Task 4 creates `robotPositionPersist.ts` + modifies `SessionRobot.tsx` (init from persistence) + `robotPositionStore.ts` + `CyberdromeScene.tsx` (save loop)
696
+ - **Conflict on SessionRobot.tsx and CyberdromeScene.tsx** — must coordinate. Task 2 changes NavState structure + useFrame navigation; Task 4 changes NavState init + adds persistence write. Run sequentially with Task 2 first.
697
+
698
+ **Phase 3** (sequential): Task 5
699
+ - Modifies `cyberdromeScene.ts`, `CyberdromeEnvironment.tsx`, `RoomLabels.tsx`, `CyberdromeScene.tsx`
700
+ - Must run after Task 2 (which also modifies `cyberdromeScene.ts`)
701
+
702
+ **Recommended serial order if not parallelizing:**
703
+ 1. Task 1 — smallest
704
+ 2. Task 3 — fixes critical crash
705
+ 3. Task 6 — speed increase (small, isolated)
706
+ 4. Task 2 — navigation overhaul
707
+ 5. Task 4 — persistence (depends on Task 2's NavState changes)
708
+ 6. Task 5 — largest, content + relocation
709
+
710
+ ## Build Commands
711
+
712
+ ```bash
713
+ npx tsc --noEmit # Type check
714
+ npx vitest run # Test suite (407 tests)
715
+ npx vite build # Production build to dist/
716
+ ```