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.
- claude_jacked-0.2.9.dist-info/METADATA +523 -0
- claude_jacked-0.2.9.dist-info/RECORD +33 -0
- jacked/cli.py +752 -47
- jacked/client.py +196 -29
- jacked/data/agents/code-simplicity-reviewer.md +87 -0
- jacked/data/agents/defensive-error-handler.md +93 -0
- jacked/data/agents/double-check-reviewer.md +214 -0
- jacked/data/agents/git-pr-workflow-manager.md +149 -0
- jacked/data/agents/issue-pr-coordinator.md +131 -0
- jacked/data/agents/pr-workflow-checker.md +199 -0
- jacked/data/agents/readme-maintainer.md +123 -0
- jacked/data/agents/test-coverage-engineer.md +155 -0
- jacked/data/agents/test-coverage-improver.md +139 -0
- jacked/data/agents/wiki-documentation-architect.md +580 -0
- jacked/data/commands/audit-rules.md +103 -0
- jacked/data/commands/dc.md +155 -0
- jacked/data/commands/learn.md +89 -0
- jacked/data/commands/pr.md +4 -0
- jacked/data/commands/redo.md +85 -0
- jacked/data/commands/techdebt.md +115 -0
- jacked/data/prompts/security_gatekeeper.txt +58 -0
- jacked/data/rules/jacked_behaviors.md +11 -0
- jacked/data/skills/jacked/SKILL.md +162 -0
- jacked/index_write_tracker.py +227 -0
- jacked/indexer.py +255 -129
- jacked/retriever.py +389 -137
- jacked/searcher.py +65 -13
- jacked/transcript.py +339 -0
- claude_jacked-0.2.3.dist-info/METADATA +0 -483
- claude_jacked-0.2.3.dist-info/RECORD +0 -13
- {claude_jacked-0.2.3.dist-info → claude_jacked-0.2.9.dist-info}/WHEEL +0 -0
- {claude_jacked-0.2.3.dist-info → claude_jacked-0.2.9.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
166
|
+
Retrieves session context from Qdrant with smart mode support.
|
|
45
167
|
|
|
46
|
-
|
|
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(
|
|
196
|
+
def retrieve(
|
|
197
|
+
self,
|
|
198
|
+
session_id: str,
|
|
199
|
+
mode: RetrievalMode = "smart",
|
|
200
|
+
) -> Optional[RetrievedSession]:
|
|
70
201
|
"""
|
|
71
|
-
Retrieve a session's
|
|
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
|
|
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
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
+
mode: RetrievalMode = "smart",
|
|
336
|
+
max_tokens: int = DEFAULT_MAX_TOKENS,
|
|
215
337
|
) -> str:
|
|
216
338
|
"""
|
|
217
|
-
Format the
|
|
339
|
+
Format the session context for injection into a conversation.
|
|
218
340
|
|
|
219
341
|
Args:
|
|
220
342
|
session: RetrievedSession object
|
|
221
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
|
|
410
|
+
if session.content.plan:
|
|
411
|
+
parts.append(f"[PLAN]\n{session.content.plan}\n")
|
|
242
412
|
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
439
|
+
Format smart mode with token budgeting.
|
|
257
440
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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)
|