/ core / attention / context_bootstrap.py
context_bootstrap.py
  1  """
  2  Context Bootstrap - Fast context loading for new sessions.
  3  
  4  Problem: When you open a new Claude session, it starts cold.
  5  You have to re-explain context, re-establish what you're working on.
  6  
  7  Solution: The attention system has been tracking all your sessions.
  8  When a new session starts, bootstrap it with distilled context from
  9  recent activity across all sessions.
 10  
 11  This enables:
 12  1. Cold start → warm start in seconds
 13  2. Branch sessions that share foundation
 14  3. Resume work without re-explaining
 15  4. Context continuity across session restarts
 16  
 17  Usage:
 18      bootstrap = ContextBootstrap(sessions_dir, daily_dir)
 19  
 20      # Get briefing for a new session
 21      briefing = bootstrap.get_briefing(
 22          hours_back=4,
 23          max_tokens=2000
 24      )
 25  
 26      # Inject into session as system context
 27      print(briefing.to_markdown())
 28  """
 29  
 30  from dataclasses import dataclass, field
 31  from datetime import datetime, timedelta
 32  from typing import Optional, List, Dict, Tuple
 33  from pathlib import Path
 34  import re
 35  
 36  from .cross_session import create_cross_session_system, UnifiedAttentionState
 37  from .coherence_risk import create_coherence_system, DecisionType
 38  
 39  
 40  @dataclass
 41  class ContextBriefing:
 42      """
 43      A briefing to bootstrap a new session with context.
 44  
 45      Contains distilled knowledge from recent sessions:
 46      - What topics were active
 47      - What decisions were made
 48      - What questions are unresolved
 49      - What direction attention was heading
 50      """
 51      generated_at: datetime = field(default_factory=datetime.now)
 52  
 53      # Time window covered
 54      hours_covered: float = 4.0
 55      sessions_analyzed: int = 0
 56  
 57      # Active topics (what you were thinking about)
 58      hot_topics: List[str] = field(default_factory=list)
 59      cross_session_topics: List[str] = field(default_factory=list)
 60  
 61      # Decisions made (so new session knows the choices)
 62      recent_decisions: List[Dict] = field(default_factory=list)
 63  
 64      # Unresolved items (what still needs attention)
 65      unresolved: List[str] = field(default_factory=list)
 66  
 67      # Trajectory (where attention was heading)
 68      trajectory_summary: str = ""
 69  
 70      # Key insights/ahas detected
 71      recent_ahas: List[Dict] = field(default_factory=list)
 72  
 73      # Session summaries (brief per-session context)
 74      session_summaries: List[Dict] = field(default_factory=list)
 75  
 76      def to_markdown(self) -> str:
 77          """Convert briefing to markdown for injection."""
 78          lines = [
 79              "## Context Briefing",
 80              f"*Generated from {self.sessions_analyzed} sessions over {self.hours_covered:.1f} hours*",
 81              ""
 82          ]
 83  
 84          # Hot topics
 85          if self.hot_topics:
 86              lines.append("### Active Topics")
 87              lines.append(f"Currently working on: **{', '.join(self.hot_topics[:5])}**")
 88              if self.cross_session_topics:
 89                  lines.append(f"Cross-session focus: {', '.join(self.cross_session_topics[:3])}")
 90              lines.append("")
 91  
 92          # Recent decisions
 93          if self.recent_decisions:
 94              lines.append("### Recent Decisions")
 95              for d in self.recent_decisions[:5]:
 96                  dtype = d.get('type', 'decision')
 97                  topic = d.get('topic', '')
 98                  content = d.get('content', '')[:60]
 99                  lines.append(f"- [{dtype}] **{topic}**: {content}...")
100              lines.append("")
101  
102          # Unresolved
103          if self.unresolved:
104              lines.append("### Unresolved Items")
105              for item in self.unresolved[:5]:
106                  lines.append(f"- [ ] {item}")
107              lines.append("")
108  
109          # Trajectory
110          if self.trajectory_summary:
111              lines.append("### Direction")
112              lines.append(self.trajectory_summary)
113              lines.append("")
114  
115          # Ahas
116          if self.recent_ahas:
117              lines.append("### Recent Insights")
118              for aha in self.recent_ahas[:3]:
119                  emoji = "💡" if aha.get('type') == 'discovery' else "🔧"
120                  lines.append(f"- {emoji} {aha.get('summary', '')[:80]}")
121              lines.append("")
122  
123          return '\n'.join(lines)
124  
125      def to_system_message(self) -> str:
126          """Convert to system message format for Claude."""
127          md = self.to_markdown()
128          return f"""<context-bootstrap>
129  {md}
130  </context-bootstrap>
131  
132  You are continuing work from previous sessions. The above briefing summarizes recent context.
133  Use this to orient yourself, but the user may want to go in a different direction."""
134  
135  
136  class ContextBootstrap:
137      """
138      Generates context briefings from recent session history.
139  
140      Analyzes session streams, extracts key information, and
141      produces a briefing that can bootstrap a new session.
142      """
143  
144      def __init__(
145          self,
146          sessions_dir: str,
147          daily_notes_dir: str
148      ):
149          self.sessions_dir = Path(sessions_dir)
150          self.daily_notes_dir = Path(daily_notes_dir)
151  
152          # Initialize trackers
153          self.cross_tracker, self.session_watcher = create_cross_session_system(
154              str(sessions_dir)
155          )
156          self.coherence_detector, _ = create_coherence_system()
157  
158          # Process current state
159          self.session_watcher.scan_sessions()
160          self.session_watcher.process_updates()
161  
162      def get_briefing(
163          self,
164          hours_back: float = 4.0,
165          max_tokens: int = 2000,
166          focus_session: str = None
167      ) -> ContextBriefing:
168          """
169          Generate a context briefing from recent sessions.
170  
171          Args:
172              hours_back: How far back to look
173              max_tokens: Approximate token budget for briefing
174              focus_session: Optional session to focus on
175  
176          Returns:
177              ContextBriefing ready for injection
178          """
179          cutoff = datetime.now() - timedelta(hours=hours_back)
180  
181          # Get cross-session state
182          state = self.cross_tracker.get_state()
183  
184          # Filter to recent sessions
185          recent_sessions = {
186              sid: info for sid, info in state.sessions.items()
187              if info.last_activity and info.last_activity > cutoff
188          }
189  
190          briefing = ContextBriefing(
191              hours_covered=hours_back,
192              sessions_analyzed=len(recent_sessions)
193          )
194  
195          # Extract hot topics from sessions
196          all_topics = []
197          for info in recent_sessions.values():
198              all_topics.extend(info.topics)
199  
200          # Count topic frequency
201          topic_counts = {}
202          for topic in all_topics:
203              if topic and len(topic) > 2:
204                  topic_counts[topic] = topic_counts.get(topic, 0) + 1
205  
206          # Sort by frequency
207          sorted_topics = sorted(topic_counts.items(), key=lambda x: x[1], reverse=True)
208          briefing.hot_topics = [t for t, c in sorted_topics[:10]]
209  
210          # Cross-session attractors
211          briefing.cross_session_topics = [
212              a for a in state.cross_session_attractors
213              if a and len(a) > 2 and '\n' not in a
214          ][:5]
215  
216          # Extract decisions from session content
217          briefing.recent_decisions = self._extract_recent_decisions(
218              recent_sessions, cutoff
219          )
220  
221          # Build trajectory summary
222          if briefing.hot_topics:
223              briefing.trajectory_summary = self._build_trajectory_summary(
224                  briefing.hot_topics,
225                  briefing.cross_session_topics
226              )
227  
228          # Session summaries
229          briefing.session_summaries = self._build_session_summaries(
230              recent_sessions, max_per_session=100
231          )
232  
233          return briefing
234  
235      def _extract_recent_decisions(
236          self,
237          sessions: Dict,
238          cutoff: datetime
239      ) -> List[Dict]:
240          """Extract decisions from recent sessions."""
241          decisions = []
242  
243          # Decision patterns
244          patterns = [
245              (r"(?:decided|choosing|going with)\s+(.{10,60})", "decision"),
246              (r"(?:principle|rule):\s*(.{10,60})", "principle"),
247              (r"(?:architecture|design):\s*(.{10,60})", "architecture"),
248          ]
249  
250          for sid, info in sessions.items():
251              # Read session file
252              session_files = list(self.sessions_dir.glob(f"*{sid}*-live.md"))
253              if not session_files:
254                  continue
255  
256              try:
257                  content = session_files[0].read_text()
258              except:
259                  continue
260  
261              # Only look at recent content (last 20% of file or last 10KB)
262              if len(content) > 10000:
263                  content = content[-10000:]
264  
265              for pattern, dtype in patterns:
266                  matches = re.finditer(pattern, content, re.IGNORECASE)
267                  for match in matches:
268                      text = match.group(1).strip()
269                      if len(text) > 10:
270                          decisions.append({
271                              'type': dtype,
272                              'topic': self._extract_topic(content, match.start()),
273                              'content': text,
274                              'session': sid[:8]
275                          })
276  
277          # Deduplicate
278          seen = set()
279          unique = []
280          for d in decisions:
281              key = d['content'][:30].lower()
282              if key not in seen:
283                  seen.add(key)
284                  unique.append(d)
285  
286          return unique[:10]
287  
288      def _extract_topic(self, content: str, position: int) -> str:
289          """Extract likely topic from context around a position."""
290          start = max(0, position - 100)
291          end = min(len(content), position + 100)
292          context = content[start:end]
293  
294          # Look for code terms
295          terms = re.findall(r'\b([a-z]+_[a-z_]+)\b', context)
296          if terms:
297              return terms[0]
298  
299          # Look for hashtags
300          tags = re.findall(r'#(\w+)', context)
301          if tags:
302              return tags[0]
303  
304          return "general"
305  
306      def _build_trajectory_summary(
307          self,
308          hot_topics: List[str],
309          cross_topics: List[str]
310      ) -> str:
311          """Build a summary of where attention is heading."""
312          if cross_topics:
313              return f"Converging on: **{cross_topics[0]}**. Related: {', '.join(hot_topics[:3])}"
314          elif hot_topics:
315              return f"Exploring: {', '.join(hot_topics[:3])}"
316          return "No clear direction detected"
317  
318      def _build_session_summaries(
319          self,
320          sessions: Dict,
321          max_per_session: int = 100
322      ) -> List[Dict]:
323          """Build brief summaries of each session."""
324          summaries = []
325  
326          for sid, info in sessions.items():
327              topics = list(info.topics)[:5]
328              summaries.append({
329                  'session': sid[:8],
330                  'topics': topics,
331                  'atoms': info.atom_count,
332                  'last_active': info.last_activity.strftime('%H:%M') if info.last_activity else '?'
333              })
334  
335          return summaries
336  
337  
338  def create_bootstrap_system(
339      sessions_dir: str,
340      daily_notes_dir: str
341  ) -> ContextBootstrap:
342      """Create a context bootstrap system."""
343      return ContextBootstrap(sessions_dir, daily_notes_dir)
344  
345  
346  # Quick command to get a briefing
347  def get_quick_briefing() -> str:
348      """Get a quick briefing for the current moment."""
349      bootstrap = ContextBootstrap(
350          sessions_dir='/Users/rcerf/repos/Sovereign_Estate/daily/sessions/',
351          daily_notes_dir='/Users/rcerf/repos/Sovereign_Estate/daily/'
352      )
353      briefing = bootstrap.get_briefing(hours_back=4)
354      return briefing.to_markdown()
355  
356  
357  if __name__ == "__main__":
358      print("=== Context Bootstrap ===\n")
359  
360      bootstrap = ContextBootstrap(
361          sessions_dir='/Users/rcerf/repos/Sovereign_Estate/daily/sessions/',
362          daily_notes_dir='/Users/rcerf/repos/Sovereign_Estate/daily/'
363      )
364  
365      briefing = bootstrap.get_briefing(hours_back=8)
366  
367      print(f"Sessions analyzed: {briefing.sessions_analyzed}")
368      print(f"Hours covered: {briefing.hours_covered}")
369      print()
370      print(briefing.to_markdown())
371      print()
372      print("--- System Message Format ---")
373      print(briefing.to_system_message()[:500] + "...")