/ core / flight / session.py
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 ===")