session.py
1 """ 2 Session Manager 3 4 Manages individual work sessions within the flight protocol. 5 A session is a focused work period with clear boundaries. 6 7 Sessions integrate: 8 - Flight phase awareness 9 - Altitude tracking 10 - OODA loop seeding 11 - Resonance capture 12 """ 13 14 from dataclasses import dataclass, field 15 from datetime import datetime, timedelta 16 from enum import Enum 17 from typing import Optional, List, Dict, Any 18 import uuid 19 20 from .protocol import FlightPhase, FlightProtocol 21 from ..altitude import Altitude, AltitudeDetector, OperatorAltitudeState 22 23 24 class SessionState(Enum): 25 """Possible session states.""" 26 INITIALIZING = "initializing" 27 ACTIVE = "active" 28 PAUSED = "paused" 29 CLOSING = "closing" 30 CLOSED = "closed" 31 32 33 @dataclass 34 class SessionMoment: 35 """ 36 A captured moment within a session. 37 38 These are the atomic captures that feed the God Database. 39 """ 40 moment_id: str 41 content: str 42 timestamp: datetime 43 altitude: Optional[Altitude] = None 44 resonance_marker: Optional[float] = None # Operator's explicit marking 45 auto_resonance: Optional[float] = None # System-computed resonance 46 tags: List[str] = field(default_factory=list) 47 metadata: Dict[str, Any] = field(default_factory=dict) 48 49 50 @dataclass 51 class OODALoopRecord: 52 """ 53 Records an OODA loop within a session. 54 55 Each loop has: 56 - Observe: What came in (context, question, stimulus) 57 - Orient: How it was framed (altitude, topic, relevance) 58 - Decide: What choice was made 59 - Act: What action resulted 60 """ 61 loop_id: str 62 started_at: datetime 63 observe: str # Input/stimulus 64 orient: Optional[str] = None 65 decide: Optional[str] = None 66 act: Optional[str] = None 67 completed_at: Optional[datetime] = None 68 seeding_state: Optional[Dict[str, Any]] = None # State for next loop 69 70 71 @dataclass 72 class Session: 73 """ 74 A focused work session. 75 """ 76 session_id: str 77 operator_id: str 78 started_at: datetime 79 state: SessionState 80 flight_phase: FlightPhase 81 blanket_id: str 82 83 # Session content 84 moments: List[SessionMoment] = field(default_factory=list) 85 ooda_loops: List[OODALoopRecord] = field(default_factory=list) 86 87 # Session metadata 88 topic: Optional[str] = None 89 altitude_state: Optional[OperatorAltitudeState] = None 90 closed_at: Optional[datetime] = None 91 summary: Optional[str] = None 92 93 def duration(self) -> timedelta: 94 """Get session duration.""" 95 end = self.closed_at or datetime.now() 96 return end - self.started_at 97 98 def moment_count(self) -> int: 99 """Number of captured moments.""" 100 return len(self.moments) 101 102 def completed_loops(self) -> int: 103 """Number of completed OODA loops.""" 104 return sum(1 for loop in self.ooda_loops if loop.completed_at) 105 106 107 class SessionManager: 108 """ 109 Manages work sessions. 110 111 Responsibilities: 112 - Start/pause/close sessions 113 - Capture moments 114 - Track OODA loops 115 - Integrate with flight protocol 116 - Prepare session for export to God Database 117 """ 118 119 def __init__( 120 self, 121 flight_protocol: Optional[FlightProtocol] = None, 122 altitude_detector: Optional[AltitudeDetector] = None 123 ): 124 """ 125 Initialize the session manager. 126 127 Args: 128 flight_protocol: Optional flight protocol instance 129 altitude_detector: Optional altitude detector 130 """ 131 self.flight_protocol = flight_protocol 132 self.altitude_detector = altitude_detector or AltitudeDetector() 133 self.active_sessions: Dict[str, Session] = {} 134 self.closed_sessions: List[Session] = [] 135 136 def start_session( 137 self, 138 operator_id: str, 139 blanket_id: str, 140 topic: Optional[str] = None, 141 initial_context: Optional[str] = None 142 ) -> Session: 143 """ 144 Start a new session. 145 146 Args: 147 operator_id: Who is working 148 blanket_id: Which Markov blanket 149 topic: Optional session topic 150 initial_context: Optional starting context 151 152 Returns: 153 New Session instance 154 """ 155 session_id = str(uuid.uuid4())[:8] 156 157 # Determine flight phase 158 flight_phase = FlightPhase.FLY_HIGH 159 if self.flight_protocol: 160 flight_phase = self.flight_protocol.state.current_phase 161 162 # Create session 163 session = Session( 164 session_id=session_id, 165 operator_id=operator_id, 166 started_at=datetime.now(), 167 state=SessionState.ACTIVE, 168 flight_phase=flight_phase, 169 blanket_id=blanket_id, 170 topic=topic, 171 altitude_state=OperatorAltitudeState( 172 operator_id=operator_id, 173 current_altitude=Altitude.TACTICAL # Default 174 ) 175 ) 176 177 # If initial context provided, detect its altitude 178 if initial_context: 179 detection = self.altitude_detector.detect(initial_context) 180 session.altitude_state.current_altitude = detection.primary_altitude 181 182 self.active_sessions[session_id] = session 183 return session 184 185 def capture_moment( 186 self, 187 session_id: str, 188 content: str, 189 resonance_marker: Optional[float] = None, 190 tags: Optional[List[str]] = None 191 ) -> Optional[SessionMoment]: 192 """ 193 Capture a moment within a session. 194 195 Args: 196 session_id: Session to capture in 197 content: Content to capture 198 resonance_marker: Optional explicit resonance (0-1) 199 tags: Optional tags 200 201 Returns: 202 Created SessionMoment or None if session not found 203 """ 204 session = self.active_sessions.get(session_id) 205 if not session or session.state != SessionState.ACTIVE: 206 return None 207 208 # Detect altitude 209 detection = self.altitude_detector.detect(content, tags=tags or []) 210 211 moment = SessionMoment( 212 moment_id=str(uuid.uuid4())[:8], 213 content=content, 214 timestamp=datetime.now(), 215 altitude=detection.primary_altitude, 216 resonance_marker=resonance_marker, 217 tags=tags or [], 218 metadata={ 219 'altitude_confidence': detection.confidence, 220 'altitude_distribution': { 221 a.name: p for a, p in detection.altitude_distribution.items() 222 } 223 } 224 ) 225 226 session.moments.append(moment) 227 228 # Update altitude state if changed 229 if session.altitude_state: 230 if detection.primary_altitude != session.altitude_state.current_altitude: 231 session.altitude_state.transition_to(detection.primary_altitude) 232 233 return moment 234 235 def start_ooda_loop( 236 self, 237 session_id: str, 238 observe: str 239 ) -> Optional[OODALoopRecord]: 240 """ 241 Start a new OODA loop. 242 243 Args: 244 session_id: Session to add loop to 245 observe: Initial observation/stimulus 246 247 Returns: 248 Created OODALoopRecord or None 249 """ 250 session = self.active_sessions.get(session_id) 251 if not session or session.state != SessionState.ACTIVE: 252 return None 253 254 loop = OODALoopRecord( 255 loop_id=str(uuid.uuid4())[:8], 256 started_at=datetime.now(), 257 observe=observe 258 ) 259 260 session.ooda_loops.append(loop) 261 return loop 262 263 def update_ooda_loop( 264 self, 265 session_id: str, 266 loop_id: str, 267 orient: Optional[str] = None, 268 decide: Optional[str] = None, 269 act: Optional[str] = None 270 ) -> bool: 271 """ 272 Update an OODA loop. 273 274 Args: 275 session_id: Session containing loop 276 loop_id: Loop to update 277 orient, decide, act: Optional phase updates 278 279 Returns: 280 True if updated 281 """ 282 session = self.active_sessions.get(session_id) 283 if not session: 284 return False 285 286 for loop in session.ooda_loops: 287 if loop.loop_id == loop_id: 288 if orient: 289 loop.orient = orient 290 if decide: 291 loop.decide = decide 292 if act: 293 loop.act = act 294 # Act completes the loop 295 loop.completed_at = datetime.now() 296 return True 297 298 return False 299 300 def seed_next_loop( 301 self, 302 session_id: str, 303 seeding_state: Dict[str, Any] 304 ) -> bool: 305 """ 306 Seed state for the next OODA loop. 307 308 This captures the "How Are You Feeling" before closing a loop, 309 preparing context for the next iteration. 310 311 Args: 312 session_id: Session to seed 313 seeding_state: State to carry forward 314 315 Returns: 316 True if seeded 317 """ 318 session = self.active_sessions.get(session_id) 319 if not session or not session.ooda_loops: 320 return False 321 322 # Attach to most recent loop 323 session.ooda_loops[-1].seeding_state = seeding_state 324 return True 325 326 def pause_session(self, session_id: str) -> bool: 327 """Pause an active session.""" 328 session = self.active_sessions.get(session_id) 329 if session and session.state == SessionState.ACTIVE: 330 session.state = SessionState.PAUSED 331 return True 332 return False 333 334 def resume_session(self, session_id: str) -> bool: 335 """Resume a paused session.""" 336 session = self.active_sessions.get(session_id) 337 if session and session.state == SessionState.PAUSED: 338 session.state = SessionState.ACTIVE 339 return True 340 return False 341 342 def close_session( 343 self, 344 session_id: str, 345 summary: Optional[str] = None 346 ) -> Optional[Session]: 347 """ 348 Close a session. 349 350 Args: 351 session_id: Session to close 352 summary: Optional summary 353 354 Returns: 355 Closed session or None 356 """ 357 session = self.active_sessions.get(session_id) 358 if not session: 359 return None 360 361 session.state = SessionState.CLOSED 362 session.closed_at = datetime.now() 363 session.summary = summary 364 365 # Move to closed list 366 del self.active_sessions[session_id] 367 self.closed_sessions.append(session) 368 369 return session 370 371 def get_session_export(self, session_id: str) -> Optional[Dict[str, Any]]: 372 """ 373 Export session for God Database ingestion. 374 375 Returns: 376 Dict suitable for database import 377 """ 378 session = self.active_sessions.get(session_id) 379 if not session: 380 # Check closed sessions 381 for s in self.closed_sessions: 382 if s.session_id == session_id: 383 session = s 384 break 385 386 if not session: 387 return None 388 389 return { 390 'session_id': session.session_id, 391 'operator_id': session.operator_id, 392 'blanket_id': session.blanket_id, 393 'started_at': session.started_at.isoformat(), 394 'closed_at': session.closed_at.isoformat() if session.closed_at else None, 395 'duration_minutes': int(session.duration().total_seconds() / 60), 396 'flight_phase': session.flight_phase.value, 397 'topic': session.topic, 398 'summary': session.summary, 399 'moment_count': session.moment_count(), 400 'completed_loops': session.completed_loops(), 401 'moments': [ 402 { 403 'moment_id': m.moment_id, 404 'content': m.content, 405 'timestamp': m.timestamp.isoformat(), 406 'altitude': m.altitude.name if m.altitude else None, 407 'resonance_marker': m.resonance_marker, 408 'tags': m.tags, 409 } 410 for m in session.moments 411 ], 412 'ooda_loops': [ 413 { 414 'loop_id': l.loop_id, 415 'started_at': l.started_at.isoformat(), 416 'observe': l.observe, 417 'orient': l.orient, 418 'decide': l.decide, 419 'act': l.act, 420 'completed_at': l.completed_at.isoformat() if l.completed_at else None, 421 'seeding_state': l.seeding_state, 422 } 423 for l in session.ooda_loops 424 ], 425 'altitude_history': [ 426 { 427 'timestamp': t[0].isoformat(), 428 'altitude': t[1].name 429 } 430 for t in (session.altitude_state.altitude_history if session.altitude_state else []) 431 ] 432 } 433 434 435 # Quick test 436 if __name__ == "__main__": 437 print("=== Session Manager Test ===\n") 438 439 # Create manager 440 manager = SessionManager() 441 442 # Start session 443 session = manager.start_session( 444 operator_id="rick", 445 blanket_id="sovereign_estate", 446 topic="architecture design", 447 initial_context="Let's think about the overall system design" 448 ) 449 450 print(f"Session started: {session.session_id}") 451 print(f"Initial altitude: {session.altitude_state.current_altitude.name}") 452 print() 453 454 # Capture some moments 455 print("Capturing moments:") 456 457 moments_data = [ 458 ("We need a flexible architecture that can evolve", None, ["#architecture"]), 459 ("The key decision is whether to use microservices or monolith", 0.8, ["#decision"]), 460 ("Why does modularity matter? It's about managing change.", 0.9, ["#principle"]), 461 ("Step 1: Set up the database schema", None, ["#task"]), 462 ] 463 464 for content, resonance, tags in moments_data: 465 moment = manager.capture_moment( 466 session.session_id, 467 content, 468 resonance_marker=resonance, 469 tags=tags 470 ) 471 print(f" [{moment.altitude.name}] {content[:40]}...") 472 473 print() 474 475 # Start and complete an OODA loop 476 print("OODA Loop:") 477 loop = manager.start_ooda_loop( 478 session.session_id, 479 observe="User wants to design the database layer" 480 ) 481 print(f" Observe: {loop.observe}") 482 483 manager.update_ooda_loop( 484 session.session_id, 485 loop.loop_id, 486 orient="This is a strategic decision about data modeling" 487 ) 488 print(f" Orient: This is a strategic decision about data modeling") 489 490 manager.update_ooda_loop( 491 session.session_id, 492 loop.loop_id, 493 decide="Use document-oriented approach for flexibility" 494 ) 495 print(f" Decide: Use document-oriented approach for flexibility") 496 497 manager.update_ooda_loop( 498 session.session_id, 499 loop.loop_id, 500 act="Implemented God Database with SQLite + JSON" 501 ) 502 print(f" Act: Implemented God Database with SQLite + JSON") 503 504 # Seed next loop 505 manager.seed_next_loop( 506 session.session_id, 507 seeding_state={ 508 'energy_level': 'medium', 509 'satisfaction': 'high', 510 'next_focus': 'resonance scoring' 511 } 512 ) 513 print(f" Seeded: next_focus = resonance scoring") 514 515 print() 516 517 # Close session 518 closed = manager.close_session( 519 session.session_id, 520 summary="Designed initial database architecture" 521 ) 522 523 print(f"Session closed: {closed.session_id}") 524 print(f"Duration: {int(closed.duration().total_seconds())} seconds") 525 print(f"Moments captured: {closed.moment_count()}") 526 print(f"OODA loops completed: {closed.completed_loops()}") 527 528 # Export 529 print("\nSession export (truncated):") 530 export = manager.get_session_export(closed.session_id) 531 print(f" operator_id: {export['operator_id']}") 532 print(f" topic: {export['topic']}") 533 print(f" moment_count: {export['moment_count']}") 534 print(f" completed_loops: {export['completed_loops']}") 535 536 print("\n=== Test Complete ===")