claude-jacked 0.2.3__py3-none-any.whl → 0.2.9__py3-none-any.whl

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 (33) hide show
  1. claude_jacked-0.2.9.dist-info/METADATA +523 -0
  2. claude_jacked-0.2.9.dist-info/RECORD +33 -0
  3. jacked/cli.py +752 -47
  4. jacked/client.py +196 -29
  5. jacked/data/agents/code-simplicity-reviewer.md +87 -0
  6. jacked/data/agents/defensive-error-handler.md +93 -0
  7. jacked/data/agents/double-check-reviewer.md +214 -0
  8. jacked/data/agents/git-pr-workflow-manager.md +149 -0
  9. jacked/data/agents/issue-pr-coordinator.md +131 -0
  10. jacked/data/agents/pr-workflow-checker.md +199 -0
  11. jacked/data/agents/readme-maintainer.md +123 -0
  12. jacked/data/agents/test-coverage-engineer.md +155 -0
  13. jacked/data/agents/test-coverage-improver.md +139 -0
  14. jacked/data/agents/wiki-documentation-architect.md +580 -0
  15. jacked/data/commands/audit-rules.md +103 -0
  16. jacked/data/commands/dc.md +155 -0
  17. jacked/data/commands/learn.md +89 -0
  18. jacked/data/commands/pr.md +4 -0
  19. jacked/data/commands/redo.md +85 -0
  20. jacked/data/commands/techdebt.md +115 -0
  21. jacked/data/prompts/security_gatekeeper.txt +58 -0
  22. jacked/data/rules/jacked_behaviors.md +11 -0
  23. jacked/data/skills/jacked/SKILL.md +162 -0
  24. jacked/index_write_tracker.py +227 -0
  25. jacked/indexer.py +255 -129
  26. jacked/retriever.py +389 -137
  27. jacked/searcher.py +65 -13
  28. jacked/transcript.py +339 -0
  29. claude_jacked-0.2.3.dist-info/METADATA +0 -483
  30. claude_jacked-0.2.3.dist-info/RECORD +0 -13
  31. {claude_jacked-0.2.3.dist-info → claude_jacked-0.2.9.dist-info}/WHEEL +0 -0
  32. {claude_jacked-0.2.3.dist-info → claude_jacked-0.2.9.dist-info}/entry_points.txt +0 -0
  33. {claude_jacked-0.2.3.dist-info → claude_jacked-0.2.9.dist-info}/licenses/LICENSE +0 -0
jacked/retriever.py CHANGED
@@ -1,13 +1,23 @@
1
1
  """
2
2
  Session retrieval for Jacked.
3
3
 
4
- Handles retrieving full transcripts from Qdrant for context injection.
4
+ Handles retrieving session context from Qdrant with smart mode support.
5
+
6
+ Retrieval modes:
7
+ - smart: Plan + subagent summaries + labels + first user messages (default)
8
+ - plan: Just the plan file (if exists)
9
+ - labels: Just summary labels (tiny)
10
+ - agents: All subagent summaries
11
+ - full: Everything including full transcript chunks
12
+
13
+ Token budgeting ensures context fits within limits without truncation.
5
14
  """
6
15
 
7
16
  import logging
8
- from dataclasses import dataclass
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime, timezone
9
19
  from pathlib import Path
10
- from typing import Optional
20
+ from typing import Optional, Literal
11
21
 
12
22
  from jacked.config import SmartForkConfig, get_session_dir_for_repo
13
23
  from jacked.client import QdrantSessionClient
@@ -15,35 +25,152 @@ from jacked.client import QdrantSessionClient
15
25
 
16
26
  logger = logging.getLogger(__name__)
17
27
 
28
+ # Retrieval modes
29
+ RetrievalMode = Literal["smart", "plan", "labels", "agents", "full"]
30
+
31
+ # Default token budget for context injection
32
+ DEFAULT_MAX_TOKENS = 15000
33
+ CHARS_PER_TOKEN = 4 # Approximate
34
+
35
+
36
+ @dataclass
37
+ class SessionContent:
38
+ """Content from a single session organized by type."""
39
+ plan: Optional[str] = None
40
+ subagent_summaries: list[str] = field(default_factory=list)
41
+ summary_labels: list[str] = field(default_factory=list)
42
+ user_messages: list[str] = field(default_factory=list)
43
+ chunks: list[str] = field(default_factory=list)
44
+
45
+ def estimate_tokens(self) -> dict[str, int]:
46
+ """Estimate token count for each content type."""
47
+ def _tokens(text: str) -> int:
48
+ return len(text) // CHARS_PER_TOKEN
49
+
50
+ return {
51
+ "plan": _tokens(self.plan or ""),
52
+ "subagent_summaries": sum(_tokens(s) for s in self.subagent_summaries),
53
+ "summary_labels": sum(_tokens(l) for l in self.summary_labels),
54
+ "user_messages": sum(_tokens(m) for m in self.user_messages),
55
+ "chunks": sum(_tokens(c) for c in self.chunks),
56
+ "total": (
57
+ _tokens(self.plan or "") +
58
+ sum(_tokens(s) for s in self.subagent_summaries) +
59
+ sum(_tokens(l) for l in self.summary_labels) +
60
+ sum(_tokens(m) for m in self.user_messages)
61
+ # Don't include chunks in total by default (full mode only)
62
+ ),
63
+ }
64
+
18
65
 
19
66
  @dataclass
20
67
  class RetrievedSession:
21
68
  """
22
- A retrieved session with full transcript.
69
+ A retrieved session with content organized by type.
23
70
 
24
71
  Attributes:
25
72
  session_id: The session UUID
26
73
  repo_name: Name of the repository
27
74
  repo_path: Full path to the repository
28
75
  machine: Machine name where the session was indexed
29
- full_transcript: The complete transcript text
76
+ user_name: User who created the session
77
+ timestamp: When the session was last indexed
78
+ content: SessionContent with all content types
30
79
  is_local: Whether the session exists locally (for native resume)
31
80
  local_path: Path to local session file (if exists)
81
+ slug: The session slug (links to plan file)
32
82
  """
33
83
  session_id: str
34
84
  repo_name: str
35
85
  repo_path: str
36
86
  machine: str
37
- full_transcript: str
87
+ user_name: str
88
+ timestamp: Optional[datetime]
89
+ content: SessionContent
38
90
  is_local: bool
39
91
  local_path: Optional[Path]
92
+ slug: Optional[str] = None
93
+
94
+ @property
95
+ def full_transcript(self) -> str:
96
+ """Get full transcript from chunks (backwards compatibility)."""
97
+ return "\n".join(self.content.chunks)
98
+
99
+ @property
100
+ def age_days(self) -> int:
101
+ """Get age of session in days."""
102
+ if not self.timestamp:
103
+ return 0
104
+ now = datetime.now(timezone.utc)
105
+ ts = self.timestamp
106
+ if ts.tzinfo is None:
107
+ ts = ts.replace(tzinfo=timezone.utc)
108
+ return (now - ts).days
109
+
110
+ def format_relative_time(self) -> str:
111
+ """Format timestamp as relative time (e.g., '24 days ago')."""
112
+ if not self.timestamp:
113
+ return "unknown"
114
+ days = self.age_days
115
+ if days == 0:
116
+ return "today"
117
+ elif days == 1:
118
+ return "yesterday"
119
+ elif days < 7:
120
+ return f"{days} days ago"
121
+ elif days < 30:
122
+ weeks = days // 7
123
+ return f"{weeks} week{'s' if weeks != 1 else ''} ago"
124
+ elif days < 365:
125
+ months = days // 30
126
+ return f"{months} month{'s' if months != 1 else ''} ago"
127
+ else:
128
+ years = days // 365
129
+ return f"{years} year{'s' if years != 1 else ''} ago"
130
+
131
+
132
+ def get_staleness_warning(age_days: int) -> str:
133
+ """Generate appropriate staleness warning based on age.
134
+
135
+ Args:
136
+ age_days: Age of the context in days
137
+
138
+ Returns:
139
+ Warning string or empty if recent enough
140
+ """
141
+ if age_days < 7:
142
+ return "" # No warning needed
143
+ elif age_days < 30:
144
+ return (
145
+ f"ℹ️ This context is {age_days} days old. Code may have "
146
+ "changed - verify current state if anything seems off."
147
+ )
148
+ elif age_days < 90:
149
+ return (
150
+ f"⚠️ STALENESS NOTICE: This context is {age_days} days old. "
151
+ "Code, APIs, or project structure may have changed. Use this "
152
+ "as a starting point for WHERE to look, not necessarily WHAT "
153
+ "is there now."
154
+ )
155
+ else:
156
+ return (
157
+ f"🚨 OLD CONTEXT WARNING: This context is {age_days} days old "
158
+ f"(~{age_days // 30} months). Significant changes are likely. "
159
+ "Treat this as historical reference only - re-explore the "
160
+ "codebase to understand current state before making changes."
161
+ )
40
162
 
41
163
 
42
164
  class SessionRetriever:
43
165
  """
44
- Retrieves full session transcripts from Qdrant.
166
+ Retrieves session context from Qdrant with smart mode support.
45
167
 
46
- Also checks if the session exists locally for native Claude resume.
168
+ Retrieval modes:
169
+ - smart: Plan + subagent summaries + labels + first user messages
170
+ - plan: Just the plan file (if exists)
171
+ - labels: Just summary labels (tiny)
172
+ - agents: All subagent summaries
173
+ - full: Everything including full transcript chunks
47
174
 
48
175
  Attributes:
49
176
  config: SmartForkConfig instance
@@ -52,7 +179,7 @@ class SessionRetriever:
52
179
  Examples:
53
180
  >>> config = SmartForkConfig.from_env() # doctest: +SKIP
54
181
  >>> retriever = SessionRetriever(config) # doctest: +SKIP
55
- >>> session = retriever.retrieve("abc123-uuid") # doctest: +SKIP
182
+ >>> session = retriever.retrieve("abc123-uuid", mode="smart") # doctest: +SKIP
56
183
  """
57
184
 
58
185
  def __init__(self, config: SmartForkConfig, client: Optional[QdrantSessionClient] = None):
@@ -66,21 +193,24 @@ class SessionRetriever:
66
193
  self.config = config
67
194
  self.client = client or QdrantSessionClient(config)
68
195
 
69
- def retrieve(self, session_id: str) -> Optional[RetrievedSession]:
196
+ def retrieve(
197
+ self,
198
+ session_id: str,
199
+ mode: RetrievalMode = "smart",
200
+ ) -> Optional[RetrievedSession]:
70
201
  """
71
- Retrieve a session's full transcript.
202
+ Retrieve a session's context with specified mode.
72
203
 
73
204
  Args:
74
205
  session_id: The session UUID to retrieve
206
+ mode: Retrieval mode (smart, plan, labels, agents, full)
75
207
 
76
208
  Returns:
77
209
  RetrievedSession object or None if not found
78
210
 
79
211
  Examples:
80
212
  >>> retriever = SessionRetriever(config) # doctest: +SKIP
81
- >>> session = retriever.retrieve("533e6824-6fb0-4f12-a406-517d2677734e") # doctest: +SKIP
82
- >>> if session: # doctest: +SKIP
83
- ... print(f"Found session with {len(session.full_transcript)} chars")
213
+ >>> session = retriever.retrieve("533e6824-...", mode="smart") # doctest: +SKIP
84
214
  """
85
215
  # Get all points for this session
86
216
  points = self.client.get_points_by_session(session_id)
@@ -89,96 +219,97 @@ class SessionRetriever:
89
219
  logger.warning(f"Session {session_id} not found in index")
90
220
  return None
91
221
 
92
- # Separate intent and chunk points
93
- intent_points = []
94
- chunk_points = []
222
+ # Organize points by content type
223
+ content = SessionContent()
224
+ metadata = {}
95
225
 
96
226
  for point in points:
97
227
  payload = point.payload or {}
98
- point_type = payload.get("type")
99
-
100
- if point_type == "intent":
101
- intent_points.append((payload.get("chunk_index", 0), payload))
102
- elif point_type == "chunk":
103
- chunk_points.append((payload.get("chunk_index", 0), payload))
104
-
105
- # Get metadata from first intent point
106
- repo_name = "unknown"
107
- repo_path = ""
108
- machine = "unknown"
109
-
110
- if intent_points:
111
- _, first_intent = sorted(intent_points, key=lambda x: x[0])[0]
112
- repo_name = first_intent.get("repo_name", "unknown")
113
- repo_path = first_intent.get("repo_path", "")
114
- machine = first_intent.get("machine", "unknown")
115
-
116
- # Reconstruct transcript from chunks
117
- if not chunk_points:
118
- logger.warning(f"Session {session_id} has no transcript chunks")
119
- return None
120
-
121
- # Sort chunks by index and reconstruct
122
- sorted_chunks = sorted(chunk_points, key=lambda x: x[0])
123
- full_transcript = self._reconstruct_transcript(sorted_chunks)
228
+ content_type = payload.get("content_type", payload.get("type", ""))
229
+ chunk_content = payload.get("content", "")
230
+
231
+ # Save metadata from first point
232
+ if not metadata:
233
+ metadata = {
234
+ "repo_name": payload.get("repo_name", "unknown"),
235
+ "repo_path": payload.get("repo_path", ""),
236
+ "machine": payload.get("machine", "unknown"),
237
+ "user_name": payload.get("user_name", "unknown"),
238
+ "slug": payload.get("slug"),
239
+ "timestamp": payload.get("timestamp"),
240
+ }
241
+
242
+ # Organize by content type
243
+ if content_type == "plan":
244
+ content.plan = chunk_content
245
+ elif content_type == "subagent_summary":
246
+ content.subagent_summaries.append(
247
+ (payload.get("chunk_index", 0), chunk_content)
248
+ )
249
+ elif content_type == "summary_label":
250
+ content.summary_labels.append(
251
+ (payload.get("chunk_index", 0), chunk_content)
252
+ )
253
+ elif content_type == "user_message":
254
+ content.user_messages.append(
255
+ (payload.get("chunk_index", 0), chunk_content)
256
+ )
257
+ elif content_type == "chunk":
258
+ content.chunks.append(
259
+ (payload.get("chunk_index", 0), chunk_content)
260
+ )
261
+ elif content_type == "intent":
262
+ # Legacy: treat as user_message
263
+ content.user_messages.append(
264
+ (payload.get("chunk_index", 0), payload.get("intent_text", chunk_content))
265
+ )
266
+
267
+ # Sort content by chunk_index and extract just the text
268
+ content.subagent_summaries = [
269
+ text for _, text in sorted(content.subagent_summaries, key=lambda x: x[0])
270
+ ]
271
+ content.summary_labels = [
272
+ text for _, text in sorted(content.summary_labels, key=lambda x: x[0])
273
+ ]
274
+ content.user_messages = [
275
+ text for _, text in sorted(content.user_messages, key=lambda x: x[0])
276
+ ]
277
+ content.chunks = [
278
+ text for _, text in sorted(content.chunks, key=lambda x: x[0])
279
+ ]
280
+
281
+ # Parse timestamp
282
+ timestamp = None
283
+ ts_str = metadata.get("timestamp")
284
+ if ts_str:
285
+ try:
286
+ timestamp = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
287
+ except ValueError:
288
+ pass
124
289
 
125
290
  # Check if session exists locally
291
+ repo_path = metadata.get("repo_path", "")
126
292
  is_local, local_path = self._check_local_session(session_id, repo_path)
127
293
 
128
294
  return RetrievedSession(
129
295
  session_id=session_id,
130
- repo_name=repo_name,
296
+ repo_name=metadata.get("repo_name", "unknown"),
131
297
  repo_path=repo_path,
132
- machine=machine,
133
- full_transcript=full_transcript,
298
+ machine=metadata.get("machine", "unknown"),
299
+ user_name=metadata.get("user_name", "unknown"),
300
+ timestamp=timestamp,
301
+ content=content,
134
302
  is_local=is_local,
135
303
  local_path=local_path,
304
+ slug=metadata.get("slug"),
136
305
  )
137
306
 
138
- def _reconstruct_transcript(
139
- self,
140
- sorted_chunks: list[tuple[int, dict]],
141
- ) -> str:
142
- """
143
- Reconstruct the full transcript from chunks.
144
-
145
- Handles overlap by removing duplicate content between chunks.
146
-
147
- Args:
148
- sorted_chunks: List of (index, payload) tuples, sorted by index
149
-
150
- Returns:
151
- Reconstructed transcript text
152
- """
153
- if not sorted_chunks:
154
- return ""
155
-
156
- # Simple approach: just concatenate chunks
157
- # The overlap helps ensure we don't lose content at boundaries
158
- # For retrieval, having some duplication is better than missing content
159
- parts = []
160
- for _, payload in sorted_chunks:
161
- content = payload.get("content", "")
162
- if content:
163
- parts.append(content)
164
-
165
- return "\n".join(parts)
166
-
167
307
  def _check_local_session(
168
308
  self,
169
309
  session_id: str,
170
310
  repo_path: str,
171
311
  ) -> tuple[bool, Optional[Path]]:
172
- """
173
- Check if the session exists locally.
174
-
175
- Args:
176
- session_id: The session UUID
177
- repo_path: Full path to the repository
178
-
179
- Returns:
180
- Tuple of (is_local, local_path)
181
- """
312
+ """Check if the session exists locally."""
182
313
  if not repo_path:
183
314
  return False, None
184
315
 
@@ -193,17 +324,7 @@ class SessionRetriever:
193
324
  return False, None
194
325
 
195
326
  def get_resume_command(self, session: RetrievedSession) -> Optional[str]:
196
- """
197
- Get the Claude CLI command to resume a session natively.
198
-
199
- Only works for local sessions.
200
-
201
- Args:
202
- session: RetrievedSession object
203
-
204
- Returns:
205
- CLI command string or None if not local
206
- """
327
+ """Get the Claude CLI command to resume a session natively."""
207
328
  if session.is_local:
208
329
  return f"claude --resume {session.session_id}"
209
330
  return None
@@ -211,62 +332,193 @@ class SessionRetriever:
211
332
  def format_for_injection(
212
333
  self,
213
334
  session: RetrievedSession,
214
- max_length: int = 50000,
335
+ mode: RetrievalMode = "smart",
336
+ max_tokens: int = DEFAULT_MAX_TOKENS,
215
337
  ) -> str:
216
338
  """
217
- Format the transcript for injection into a conversation.
339
+ Format the session context for injection into a conversation.
218
340
 
219
341
  Args:
220
342
  session: RetrievedSession object
221
- max_length: Maximum length of formatted output
343
+ mode: Retrieval mode determining what content to include
344
+ max_tokens: Maximum token budget (smart mode only)
222
345
 
223
346
  Returns:
224
- Formatted context string
347
+ Formatted context string with staleness warning if needed
225
348
  """
226
- header = (
227
- f"=== CONTEXT FROM PREVIOUS SESSION ===\n"
228
- f"Session: {session.session_id}\n"
229
- f"Repository: {session.repo_name}\n"
230
- f"Machine: {session.machine}\n"
231
- f"{'='*40}\n\n"
232
- )
349
+ # Build header with relative time
350
+ relative_time = session.format_relative_time()
351
+ staleness_warning = get_staleness_warning(session.age_days)
352
+
353
+ header_parts = [
354
+ "=== CONTEXT FROM PREVIOUS SESSION ===",
355
+ f"Session: {session.session_id}",
356
+ f"Repository: {session.repo_name}",
357
+ f"Machine: {session.machine}",
358
+ f"Age: {relative_time}",
359
+ ]
360
+
361
+ if staleness_warning:
362
+ header_parts.append("")
363
+ header_parts.append(staleness_warning)
364
+
365
+ header_parts.append("=" * 40)
366
+ header = "\n".join(header_parts) + "\n\n"
367
+
368
+ # Build content based on mode
369
+ if mode == "plan":
370
+ body = self._format_plan_only(session)
371
+ elif mode == "labels":
372
+ body = self._format_labels_only(session)
373
+ elif mode == "agents":
374
+ body = self._format_agents_only(session)
375
+ elif mode == "full":
376
+ body = self._format_full(session)
377
+ else: # smart
378
+ body = self._format_smart(session, max_tokens)
233
379
 
234
- # Truncate transcript if needed
235
- transcript = session.full_transcript
236
- available = max_length - len(header) - 100 # Leave room for footer
380
+ footer = f"\n{'='*40}\n=== END PREVIOUS SESSION CONTEXT ===\n"
237
381
 
238
- if len(transcript) > available:
239
- transcript = transcript[:available] + "\n... [transcript truncated]"
382
+ return header + body + footer
383
+
384
+ def _format_plan_only(self, session: RetrievedSession) -> str:
385
+ """Format plan-only mode."""
386
+ if session.content.plan:
387
+ return f"[PLAN]\n{session.content.plan}\n"
388
+ return "[No plan file found for this session]\n"
389
+
390
+ def _format_labels_only(self, session: RetrievedSession) -> str:
391
+ """Format labels-only mode."""
392
+ if session.content.summary_labels:
393
+ labels_text = "\n".join(f"• {label}" for label in session.content.summary_labels)
394
+ return f"[SUMMARY LABELS]\n{labels_text}\n"
395
+ return "[No summary labels found for this session]\n"
396
+
397
+ def _format_agents_only(self, session: RetrievedSession) -> str:
398
+ """Format agents-only mode."""
399
+ if session.content.subagent_summaries:
400
+ parts = []
401
+ for i, summary in enumerate(session.content.subagent_summaries, 1):
402
+ parts.append(f"[AGENT SUMMARY {i}]\n{summary}\n")
403
+ return "\n".join(parts)
404
+ return "[No subagent summaries found for this session]\n"
405
+
406
+ def _format_full(self, session: RetrievedSession) -> str:
407
+ """Format full mode with everything."""
408
+ parts = []
240
409
 
241
- footer = f"\n{'='*40}\n=== END PREVIOUS SESSION CONTEXT ===\n"
410
+ if session.content.plan:
411
+ parts.append(f"[PLAN]\n{session.content.plan}\n")
242
412
 
243
- return header + transcript + footer
413
+ if session.content.subagent_summaries:
414
+ for i, summary in enumerate(session.content.subagent_summaries, 1):
415
+ parts.append(f"[AGENT SUMMARY {i}]\n{summary}\n")
244
416
 
245
- def get_summary(self, session: RetrievedSession, max_lines: int = 20) -> str:
246
- """
247
- Get a brief summary of the session for display.
417
+ if session.content.summary_labels:
418
+ labels_text = "\n".join(f"• {label}" for label in session.content.summary_labels)
419
+ parts.append(f"[SUMMARY LABELS]\n{labels_text}\n")
248
420
 
249
- Args:
250
- session: RetrievedSession object
251
- max_lines: Maximum number of lines to show
421
+ if session.content.user_messages:
422
+ parts.append("[USER MESSAGES]")
423
+ for i, msg in enumerate(session.content.user_messages, 1):
424
+ parts.append(f"USER {i}: {msg[:500]}{'...' if len(msg) > 500 else ''}\n")
252
425
 
253
- Returns:
254
- Summary string
426
+ if session.content.chunks:
427
+ parts.append(f"\n[FULL TRANSCRIPT - {len(session.content.chunks)} chunks]")
428
+ for chunk in session.content.chunks:
429
+ parts.append(chunk)
430
+
431
+ return "\n".join(parts)
432
+
433
+ def _format_smart(
434
+ self,
435
+ session: RetrievedSession,
436
+ max_tokens: int = DEFAULT_MAX_TOKENS,
437
+ ) -> str:
255
438
  """
256
- lines = session.full_transcript.split("\n")
439
+ Format smart mode with token budgeting.
257
440
 
258
- # Get first and last N lines
259
- if len(lines) <= max_lines:
260
- preview = session.full_transcript
261
- else:
262
- half = max_lines // 2
263
- first_part = "\n".join(lines[:half])
264
- last_part = "\n".join(lines[-half:])
265
- preview = f"{first_part}\n\n... [{len(lines) - max_lines} lines omitted] ...\n\n{last_part}"
441
+ Priority (never truncate, just exclude lower priority items):
442
+ 1. Plan file (full)
443
+ 2. Subagent summaries (all if space)
444
+ 3. First 3 user messages
445
+ 4. Summary labels
446
+ """
447
+ parts = []
448
+ tokens_used = 0
449
+ max_chars = max_tokens * CHARS_PER_TOKEN
450
+
451
+ def _add_if_fits(text: str, label: str) -> bool:
452
+ """Add text if it fits in budget."""
453
+ nonlocal tokens_used, parts
454
+ text_tokens = len(text) // CHARS_PER_TOKEN
455
+ if tokens_used + text_tokens <= max_tokens:
456
+ parts.append(f"{label}\n{text}\n")
457
+ tokens_used += text_tokens
458
+ return True
459
+ return False
460
+
461
+ # 1. Plan file (highest priority - always include if exists)
462
+ if session.content.plan:
463
+ plan_tokens = len(session.content.plan) // CHARS_PER_TOKEN
464
+ parts.append(f"[PLAN - {plan_tokens} tokens]\n{session.content.plan}\n")
465
+ tokens_used += plan_tokens
466
+
467
+ # 2. Subagent summaries
468
+ for i, summary in enumerate(session.content.subagent_summaries, 1):
469
+ summary_tokens = len(summary) // CHARS_PER_TOKEN
470
+ if tokens_used + summary_tokens <= max_tokens:
471
+ parts.append(f"[AGENT SUMMARY {i} - {summary_tokens} tokens]\n{summary}\n")
472
+ tokens_used += summary_tokens
473
+ else:
474
+ logger.debug(f"Skipping agent summary {i} - would exceed token budget")
475
+
476
+ # 3. First 3 user messages
477
+ for i, msg in enumerate(session.content.user_messages[:3], 1):
478
+ msg_tokens = len(msg) // CHARS_PER_TOKEN
479
+ if tokens_used + msg_tokens <= max_tokens:
480
+ parts.append(f"[USER MESSAGE {i} - {msg_tokens} tokens]\n{msg}\n")
481
+ tokens_used += msg_tokens
482
+ else:
483
+ logger.debug(f"Skipping user message {i} - would exceed token budget")
484
+
485
+ # 4. Summary labels (usually small enough to fit)
486
+ if session.content.summary_labels:
487
+ labels_text = "\n".join(f"• {label}" for label in session.content.summary_labels)
488
+ labels_tokens = len(labels_text) // CHARS_PER_TOKEN
489
+ if tokens_used + labels_tokens <= max_tokens:
490
+ parts.append(f"[SUMMARY LABELS - {labels_tokens} tokens]\n{labels_text}\n")
491
+ tokens_used += labels_tokens
492
+
493
+ # Add token accounting footer
494
+ remaining = max_tokens - tokens_used
495
+ parts.append(f"\n[Token budget: {max_tokens} | Used: {tokens_used} | Remaining: {remaining}]")
266
496
 
267
- return (
268
- f"Session {session.session_id} ({session.repo_name})\n"
269
- f"From machine: {session.machine}\n"
270
- f"Local: {'Yes' if session.is_local else 'No'}\n"
271
- f"\nPreview:\n{preview}"
272
- )
497
+ return "\n".join(parts)
498
+
499
+ def get_summary(self, session: RetrievedSession, max_lines: int = 20) -> str:
500
+ """Get a brief summary of the session for display."""
501
+ parts = [
502
+ f"Session {session.session_id} ({session.repo_name})",
503
+ f"Age: {session.format_relative_time()}",
504
+ f"Machine: {session.machine}",
505
+ f"Local: {'Yes' if session.is_local else 'No'}",
506
+ "",
507
+ "Content available:",
508
+ ]
509
+
510
+ if session.content.plan:
511
+ parts.append(f" • Plan file ({len(session.content.plan)} chars)")
512
+ if session.content.subagent_summaries:
513
+ parts.append(f" • {len(session.content.subagent_summaries)} agent summaries")
514
+ if session.content.summary_labels:
515
+ parts.append(f" • {len(session.content.summary_labels)} summary labels")
516
+ if session.content.user_messages:
517
+ parts.append(f" • {len(session.content.user_messages)} user messages")
518
+ if session.content.chunks:
519
+ parts.append(f" • {len(session.content.chunks)} transcript chunks")
520
+
521
+ tokens = session.content.estimate_tokens()
522
+ parts.append(f"\nEstimated tokens (smart mode): {tokens['total']}")
523
+
524
+ return "\n".join(parts)