claude-code-tools 1.0.6__py3-none-any.whl → 1.4.1__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_code_tools/__init__.py +1 -1
- claude_code_tools/action_rpc.py +16 -10
- claude_code_tools/aichat.py +793 -51
- claude_code_tools/claude_continue.py +4 -0
- claude_code_tools/codex_continue.py +48 -0
- claude_code_tools/export_session.py +9 -5
- claude_code_tools/find_claude_session.py +36 -12
- claude_code_tools/find_codex_session.py +33 -18
- claude_code_tools/find_session.py +30 -16
- claude_code_tools/gdoc2md.py +220 -0
- claude_code_tools/md2gdoc.py +549 -0
- claude_code_tools/search_index.py +83 -9
- claude_code_tools/session_menu_cli.py +1 -1
- claude_code_tools/session_utils.py +3 -3
- claude_code_tools/smart_trim.py +18 -8
- claude_code_tools/smart_trim_core.py +4 -2
- claude_code_tools/tmux_cli_controller.py +35 -25
- claude_code_tools/trim_session.py +28 -2
- claude_code_tools-1.4.1.dist-info/METADATA +1113 -0
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.1.dist-info}/RECORD +30 -24
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.1.dist-info}/entry_points.txt +2 -0
- docs/local-llm-setup.md +286 -0
- docs/reddit-aichat-resume-v2.md +80 -0
- docs/reddit-aichat-resume.md +29 -0
- docs/reddit-aichat.md +79 -0
- docs/rollover-details.md +67 -0
- node_ui/action_config.js +3 -3
- node_ui/menu.js +67 -113
- claude_code_tools/session_tui.py +0 -516
- claude_code_tools-1.0.6.dist-info/METADATA +0 -685
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.1.dist-info}/WHEEL +0 -0
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,14 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import math
|
|
5
|
+
import shutil
|
|
5
6
|
import sys
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from datetime import datetime
|
|
9
|
+
from importlib.metadata import version as get_pkg_version
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
from typing import Any, Optional
|
|
10
12
|
|
|
11
13
|
from claude_code_tools.session_utils import is_valid_session
|
|
12
14
|
|
|
15
|
+
|
|
16
|
+
def _get_package_version() -> str:
|
|
17
|
+
"""Get installed package version for automatic index rebuilding."""
|
|
18
|
+
try:
|
|
19
|
+
return get_pkg_version("claude-code-tools")
|
|
20
|
+
except Exception:
|
|
21
|
+
return "unknown"
|
|
22
|
+
|
|
13
23
|
# Lazy imports to allow module to load even if deps not installed
|
|
14
24
|
try:
|
|
15
25
|
import yaml
|
|
@@ -176,6 +186,9 @@ class SessionIndex:
|
|
|
176
186
|
# Use "raw" tokenizer so paths are indexed as single tokens for exact matching
|
|
177
187
|
self.schema_builder.add_text_field("claude_home", stored=True, tokenizer_name="raw")
|
|
178
188
|
|
|
189
|
+
# Custom title field (from /rename command)
|
|
190
|
+
self.schema_builder.add_text_field("custom_title", stored=True)
|
|
191
|
+
|
|
179
192
|
# Searchable content field
|
|
180
193
|
self.schema_builder.add_text_field("content", stored=True)
|
|
181
194
|
|
|
@@ -184,10 +197,43 @@ class SessionIndex:
|
|
|
184
197
|
# Create or open index
|
|
185
198
|
self.index_path.mkdir(parents=True, exist_ok=True)
|
|
186
199
|
|
|
187
|
-
if
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
200
|
+
# Check index version - rebuild if package version changed
|
|
201
|
+
version_file = self.index_path / "VERSION"
|
|
202
|
+
current_version = _get_package_version()
|
|
203
|
+
needs_rebuild = False
|
|
204
|
+
|
|
205
|
+
if version_file.exists():
|
|
206
|
+
try:
|
|
207
|
+
stored_version = version_file.read_text().strip()
|
|
208
|
+
if stored_version != current_version:
|
|
209
|
+
needs_rebuild = True
|
|
210
|
+
print(
|
|
211
|
+
f"Package version changed ({stored_version} -> {current_version}), "
|
|
212
|
+
"rebuilding index..."
|
|
213
|
+
)
|
|
214
|
+
except IOError:
|
|
215
|
+
needs_rebuild = True
|
|
216
|
+
print("Index version file corrupted, rebuilding...")
|
|
217
|
+
elif (self.index_path / "meta.json").exists():
|
|
218
|
+
# Index exists but no VERSION file - legacy index, rebuild
|
|
219
|
+
needs_rebuild = True
|
|
220
|
+
print("Upgrading index to versioned format, rebuilding...")
|
|
221
|
+
|
|
222
|
+
if needs_rebuild:
|
|
223
|
+
# Clear index directory contents (but keep the directory itself)
|
|
224
|
+
for item in self.index_path.iterdir():
|
|
225
|
+
if item.is_file():
|
|
226
|
+
item.unlink()
|
|
227
|
+
elif item.is_dir():
|
|
228
|
+
shutil.rmtree(item)
|
|
229
|
+
# Reset state tracker
|
|
230
|
+
self.state = IndexState(self.index_path / "index_state.json")
|
|
231
|
+
|
|
232
|
+
# Create or open index
|
|
233
|
+
self.index = tantivy.Index(self.schema, path=str(self.index_path))
|
|
234
|
+
|
|
235
|
+
# Write current version
|
|
236
|
+
version_file.write_text(current_version)
|
|
191
237
|
|
|
192
238
|
def _parse_export_file(self, export_path: Path) -> Optional[dict[str, Any]]:
|
|
193
239
|
"""
|
|
@@ -287,6 +333,9 @@ class SessionIndex:
|
|
|
287
333
|
"true" if metadata.get("is_sidechain") else "false"
|
|
288
334
|
)
|
|
289
335
|
|
|
336
|
+
# Custom title (from /rename command)
|
|
337
|
+
doc.add_text("custom_title", metadata.get("customTitle", "") or "")
|
|
338
|
+
|
|
290
339
|
doc.add_text("content", parsed["content"])
|
|
291
340
|
|
|
292
341
|
writer.add_document(doc)
|
|
@@ -370,6 +419,9 @@ class SessionIndex:
|
|
|
370
419
|
"true" if metadata.get("is_sidechain") else "false"
|
|
371
420
|
)
|
|
372
421
|
|
|
422
|
+
# Custom title (from /rename command)
|
|
423
|
+
doc.add_text("custom_title", metadata.get("customTitle", "") or "")
|
|
424
|
+
|
|
373
425
|
doc.add_text("content", parsed["content"])
|
|
374
426
|
|
|
375
427
|
writer.add_document(doc)
|
|
@@ -426,7 +478,7 @@ class SessionIndex:
|
|
|
426
478
|
|
|
427
479
|
def _extract_session_content(
|
|
428
480
|
self, jsonl_path: Path, agent: str
|
|
429
|
-
) -> tuple[str, int]:
|
|
481
|
+
) -> tuple[str, int, str]:
|
|
430
482
|
"""
|
|
431
483
|
Extract searchable content from a session file.
|
|
432
484
|
|
|
@@ -437,10 +489,11 @@ class SessionIndex:
|
|
|
437
489
|
agent: Agent type ('claude' or 'codex')
|
|
438
490
|
|
|
439
491
|
Returns:
|
|
440
|
-
Tuple of (content_string, user_message_count)
|
|
492
|
+
Tuple of (content_string, user_message_count, custom_title)
|
|
441
493
|
"""
|
|
442
494
|
messages = []
|
|
443
495
|
user_count = 0 # Count only user messages for the "lines" metric
|
|
496
|
+
custom_title = "" # Session name from /rename command
|
|
444
497
|
|
|
445
498
|
try:
|
|
446
499
|
with open(jsonl_path, "r", encoding="utf-8") as f:
|
|
@@ -454,6 +507,11 @@ class SessionIndex:
|
|
|
454
507
|
except json.JSONDecodeError:
|
|
455
508
|
continue
|
|
456
509
|
|
|
510
|
+
# Check for custom-title (from /rename command)
|
|
511
|
+
if data.get("type") == "custom-title":
|
|
512
|
+
custom_title = data.get("customTitle", "")
|
|
513
|
+
continue
|
|
514
|
+
|
|
457
515
|
role: Optional[str] = None
|
|
458
516
|
text: str = ""
|
|
459
517
|
|
|
@@ -496,6 +554,12 @@ class SessionIndex:
|
|
|
496
554
|
elif block.get("type") == "tool_use":
|
|
497
555
|
tool_name = block.get("name", "")
|
|
498
556
|
text += f"[Tool: {tool_name}]\n"
|
|
557
|
+
# Index tool input content
|
|
558
|
+
tool_input = block.get("input", {})
|
|
559
|
+
if isinstance(tool_input, dict):
|
|
560
|
+
for value in tool_input.values():
|
|
561
|
+
if isinstance(value, str) and value:
|
|
562
|
+
text += f"{value}\n"
|
|
499
563
|
elif block.get("type") == "tool_result":
|
|
500
564
|
result = block.get("content", "")
|
|
501
565
|
if isinstance(result, str):
|
|
@@ -532,7 +596,7 @@ class SessionIndex:
|
|
|
532
596
|
except (OSError, IOError):
|
|
533
597
|
pass
|
|
534
598
|
|
|
535
|
-
return "\n\n".join(messages), user_count
|
|
599
|
+
return "\n\n".join(messages), user_count, custom_title
|
|
536
600
|
|
|
537
601
|
def _parse_jsonl_session(self, jsonl_path: Path) -> Optional[dict[str, Any]]:
|
|
538
602
|
"""
|
|
@@ -556,8 +620,14 @@ class SessionIndex:
|
|
|
556
620
|
from claude_code_tools.export_session import extract_session_metadata
|
|
557
621
|
metadata = extract_session_metadata(jsonl_path, agent)
|
|
558
622
|
|
|
559
|
-
# Extract content for full-text search
|
|
560
|
-
content, msg_count = self._extract_session_content(
|
|
623
|
+
# Extract content for full-text search (also extracts custom_title)
|
|
624
|
+
content, msg_count, custom_title = self._extract_session_content(
|
|
625
|
+
jsonl_path, agent
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
# Add custom_title to metadata (extracted during content scan)
|
|
629
|
+
if custom_title:
|
|
630
|
+
metadata["customTitle"] = custom_title
|
|
561
631
|
|
|
562
632
|
if msg_count == 0:
|
|
563
633
|
return {"_skip_reason": "empty"}
|
|
@@ -585,6 +655,7 @@ class SessionIndex:
|
|
|
585
655
|
"is_sidechain": metadata.get("is_sidechain", False),
|
|
586
656
|
"derivation_type": metadata.get("derivation_type", "") or "",
|
|
587
657
|
"session_type": metadata.get("session_type"),
|
|
658
|
+
"customTitle": metadata.get("customTitle", "") or "",
|
|
588
659
|
},
|
|
589
660
|
"content": content,
|
|
590
661
|
"first_msg": first_msg,
|
|
@@ -724,6 +795,9 @@ class SessionIndex:
|
|
|
724
795
|
"true" if metadata.get("is_sidechain") else "false"
|
|
725
796
|
)
|
|
726
797
|
|
|
798
|
+
# Custom title (from /rename command)
|
|
799
|
+
doc.add_text("custom_title", metadata.get("customTitle", "") or "")
|
|
800
|
+
|
|
727
801
|
# Source home (for filtering by source directory)
|
|
728
802
|
# Detect from path whether this is a Claude or Codex session
|
|
729
803
|
agent = metadata.get("agent", "")
|
|
@@ -604,7 +604,7 @@ def is_valid_session(filepath: Path) -> bool:
|
|
|
604
604
|
|
|
605
605
|
# Whitelist of resumable message types
|
|
606
606
|
# Claude Code types (require sessionId)
|
|
607
|
-
claude_valid_types = {"user", "assistant", "tool_result", "tool_use"}
|
|
607
|
+
claude_valid_types = {"user", "assistant", "tool_result", "tool_use", "system"}
|
|
608
608
|
# Codex types (conversation content types)
|
|
609
609
|
codex_valid_types = {"event_msg", "response_item", "turn_context"}
|
|
610
610
|
|
|
@@ -1167,7 +1167,7 @@ This session continues from a previous conversation. The prior session log is:
|
|
|
1167
1167
|
|
|
1168
1168
|
{session_file}
|
|
1169
1169
|
|
|
1170
|
-
The file is in JSONL format.
|
|
1170
|
+
The file is in JSONL format. Since it can be large, use appropriate strategies (such as sub-agents if available) to carefully explore it if you need context.
|
|
1171
1171
|
|
|
1172
1172
|
Do not do anything yet. Simply greet the user and await instructions on how they want to continue the work based on the above session."""
|
|
1173
1173
|
else:
|
|
@@ -1182,7 +1182,7 @@ This session continues from a chain of prior conversations. Here are the JSONL s
|
|
|
1182
1182
|
- "trimmed" = Long messages were truncated to free up context. Full content in parent session.
|
|
1183
1183
|
- "rolled over" = Work handed off to fresh session.
|
|
1184
1184
|
|
|
1185
|
-
|
|
1185
|
+
Since these files can be large, use appropriate strategies (such as sub-agents if available) to carefully explore them if you need context.
|
|
1186
1186
|
|
|
1187
1187
|
Do not do anything yet. Simply greet the user and await instructions on how they want to continue the work based on the above sessions."""
|
|
1188
1188
|
|
claude_code_tools/smart_trim.py
CHANGED
|
@@ -12,7 +12,7 @@ from typing import List, Optional
|
|
|
12
12
|
|
|
13
13
|
from claude_code_tools.smart_trim_core import identify_trimmable_lines_cli, SMART_TRIM_THRESHOLD
|
|
14
14
|
from claude_code_tools.trim_session import detect_agent, inject_lineage_into_first_user_message
|
|
15
|
-
from claude_code_tools.session_utils import get_claude_home, resolve_session_path
|
|
15
|
+
from claude_code_tools.session_utils import get_claude_home, get_session_uuid, resolve_session_path
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def trim_lines(
|
|
@@ -334,18 +334,28 @@ def main():
|
|
|
334
334
|
line_indices = [item[0] for item in trimmable]
|
|
335
335
|
|
|
336
336
|
# Determine output file
|
|
337
|
-
output_dir = args.output_dir or session_file.parent
|
|
338
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
|
339
|
-
|
|
340
337
|
# Use agent type already detected earlier
|
|
341
338
|
if agent == "claude":
|
|
342
|
-
#
|
|
339
|
+
# Claude: output in same directory or specified output_dir
|
|
340
|
+
output_dir = args.output_dir or session_file.parent
|
|
341
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
343
342
|
new_uuid = str(uuid.uuid4())
|
|
344
343
|
output_file = output_dir / f"{new_uuid}.jsonl"
|
|
345
344
|
else:
|
|
346
|
-
# Codex
|
|
347
|
-
|
|
345
|
+
# Codex: new session goes in today's date folder (YYYY/MM/DD)
|
|
346
|
+
now = datetime.datetime.now()
|
|
347
|
+
timestamp = now.strftime("%Y-%m-%dT%H-%M-%S")
|
|
348
|
+
date_path = now.strftime("%Y/%m/%d")
|
|
348
349
|
new_uuid = str(uuid.uuid4())
|
|
350
|
+
|
|
351
|
+
if args.output_dir:
|
|
352
|
+
output_dir = args.output_dir / date_path
|
|
353
|
+
else:
|
|
354
|
+
# Find sessions root by going up from input file (sessions/YYYY/MM/DD/file.jsonl)
|
|
355
|
+
sessions_root = session_file.parent.parent.parent.parent
|
|
356
|
+
output_dir = sessions_root / date_path
|
|
357
|
+
|
|
358
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
349
359
|
output_file = output_dir / f"rollout-{timestamp}-{new_uuid}.jsonl"
|
|
350
360
|
|
|
351
361
|
# Perform trimming (pass descriptions for truncation summaries)
|
|
@@ -407,7 +417,7 @@ def main():
|
|
|
407
417
|
print(f" Tokens saved (est): ~{stats['tokens_saved']:,}")
|
|
408
418
|
print()
|
|
409
419
|
print(f"📄 Output: {output_file}")
|
|
410
|
-
print(f" Session ID: {output_file.
|
|
420
|
+
print(f" Session ID: {get_session_uuid(output_file.name)}")
|
|
411
421
|
|
|
412
422
|
|
|
413
423
|
if __name__ == "__main__":
|
|
@@ -8,6 +8,7 @@ from typing import List, Optional, Any, Dict
|
|
|
8
8
|
from claude_code_tools.session_utils import (
|
|
9
9
|
get_codex_home,
|
|
10
10
|
get_claude_home,
|
|
11
|
+
get_session_uuid,
|
|
11
12
|
encode_claude_project_path,
|
|
12
13
|
mark_session_as_helper,
|
|
13
14
|
)
|
|
@@ -181,8 +182,8 @@ def analyze_session_with_cli(
|
|
|
181
182
|
|
|
182
183
|
session_content = "\n".join(content_parts)
|
|
183
184
|
|
|
184
|
-
# Extract session_id from filename
|
|
185
|
-
session_id = session_file.
|
|
185
|
+
# Extract session_id (UUID) from filename - works for both Claude and Codex formats
|
|
186
|
+
session_id = get_session_uuid(session_file.name)
|
|
186
187
|
|
|
187
188
|
# Determine if this is a Codex session (for output directory placement)
|
|
188
189
|
# Resolve paths to handle ~ and relative paths correctly
|
|
@@ -224,6 +225,7 @@ def analyze_session_with_cli(
|
|
|
224
225
|
result = subprocess.run(
|
|
225
226
|
[
|
|
226
227
|
"claude", "-p",
|
|
228
|
+
"--no-session-persistence",
|
|
227
229
|
"--output-format", "json",
|
|
228
230
|
"--permission-mode", "bypassPermissions",
|
|
229
231
|
prompt,
|
|
@@ -145,13 +145,19 @@ class TmuxCLIController:
|
|
|
145
145
|
|
|
146
146
|
def format_pane_identifier(self, pane_id: str) -> str:
|
|
147
147
|
"""Convert pane ID to session:window.pane format."""
|
|
148
|
+
# Validate pane_id is not empty
|
|
149
|
+
if not pane_id:
|
|
150
|
+
return pane_id
|
|
151
|
+
|
|
148
152
|
try:
|
|
149
153
|
# Get session, window index, and pane index for this pane
|
|
150
154
|
session_output, session_code = self._run_tmux_command(['display-message', '-t', pane_id, '-p', '#{session_name}'])
|
|
151
155
|
window_output, window_code = self._run_tmux_command(['display-message', '-t', pane_id, '-p', '#{window_index}'])
|
|
152
156
|
pane_output, pane_code = self._run_tmux_command(['display-message', '-t', pane_id, '-p', '#{pane_index}'])
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
|
|
158
|
+
# Check both return codes AND that outputs are non-empty
|
|
159
|
+
if (session_code == 0 and window_code == 0 and pane_code == 0 and
|
|
160
|
+
session_output and window_output and pane_output):
|
|
155
161
|
return f"{session_output}:{window_output}.{pane_output}"
|
|
156
162
|
else:
|
|
157
163
|
# Fallback to pane ID
|
|
@@ -275,31 +281,36 @@ class TmuxCLIController:
|
|
|
275
281
|
"""
|
|
276
282
|
# Get the current window ID to ensure pane is created in this window
|
|
277
283
|
current_window_id = self.get_current_window_id()
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
284
|
+
|
|
285
|
+
# Build base command
|
|
286
|
+
base_cmd = ['split-window']
|
|
287
|
+
|
|
281
288
|
# Target the specific window where tmux-cli was called from
|
|
282
289
|
if current_window_id:
|
|
283
|
-
|
|
284
|
-
|
|
290
|
+
base_cmd.extend(['-t', current_window_id])
|
|
291
|
+
|
|
285
292
|
if vertical:
|
|
286
|
-
|
|
293
|
+
base_cmd.append('-h')
|
|
287
294
|
else:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
cmd.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
295
|
+
base_cmd.append('-v')
|
|
296
|
+
|
|
297
|
+
# Try -l first (tmux 3.4+), fall back to -p (older versions)
|
|
298
|
+
# tmux 3.4 removed -p, but 3.5+ has both; older versions only have -p
|
|
299
|
+
for size_flag in (['-l', f'{size}%'], ['-p', str(size)]) if size else [[]]:
|
|
300
|
+
cmd = base_cmd.copy()
|
|
301
|
+
if size_flag:
|
|
302
|
+
cmd.extend(size_flag)
|
|
303
|
+
cmd.extend(['-P', '-F', '#{pane_id}'])
|
|
304
|
+
if start_command:
|
|
305
|
+
cmd.append(start_command)
|
|
306
|
+
|
|
307
|
+
output, code = self._run_tmux_command(cmd)
|
|
308
|
+
|
|
309
|
+
# Validate: code must be 0 and output must be a valid pane ID (starts with %)
|
|
310
|
+
if code == 0 and output and output.startswith('%'):
|
|
311
|
+
self.target_pane = output
|
|
312
|
+
return output
|
|
313
|
+
|
|
303
314
|
return None
|
|
304
315
|
|
|
305
316
|
def select_pane(self, pane_id: Optional[str] = None, pane_index: Optional[int] = None):
|
|
@@ -686,7 +697,6 @@ class CLI:
|
|
|
686
697
|
else:
|
|
687
698
|
# Remote mode - pass pane_id directly
|
|
688
699
|
content = self.controller.capture_pane(pane_id=pane, lines=lines)
|
|
689
|
-
print(content)
|
|
690
700
|
return content
|
|
691
701
|
|
|
692
702
|
def interrupt(self, pane: Optional[str] = None):
|
|
@@ -925,4 +935,4 @@ def main():
|
|
|
925
935
|
|
|
926
936
|
|
|
927
937
|
if __name__ == '__main__':
|
|
928
|
-
main()
|
|
938
|
+
main()
|
|
@@ -365,6 +365,9 @@ def create_placeholder(tool_name: str, original_length: int) -> str:
|
|
|
365
365
|
)
|
|
366
366
|
|
|
367
367
|
|
|
368
|
+
MIN_TOKEN_SAVINGS = 300 # Minimum tokens saved to consider trim worthwhile
|
|
369
|
+
|
|
370
|
+
|
|
368
371
|
def trim_and_create_session(
|
|
369
372
|
agent: Optional[str],
|
|
370
373
|
input_file: Path,
|
|
@@ -372,6 +375,7 @@ def trim_and_create_session(
|
|
|
372
375
|
threshold: int,
|
|
373
376
|
output_dir: Optional[Path] = None,
|
|
374
377
|
trim_assistant_messages: Optional[int] = None,
|
|
378
|
+
min_token_savings: int = MIN_TOKEN_SAVINGS,
|
|
375
379
|
) -> dict:
|
|
376
380
|
"""
|
|
377
381
|
Trim tool results and assistant messages, creating a new session file.
|
|
@@ -386,16 +390,19 @@ def trim_and_create_session(
|
|
|
386
390
|
- Positive N: trim first N assistant messages exceeding threshold
|
|
387
391
|
- Negative N: trim all except last abs(N) assistant messages exceeding threshold
|
|
388
392
|
- None: don't trim assistant messages
|
|
393
|
+
min_token_savings: Minimum tokens saved to consider trim worthwhile.
|
|
394
|
+
If savings are below this, deletes output file and sets nothing_to_trim=True.
|
|
389
395
|
|
|
390
396
|
Returns:
|
|
391
397
|
Dict with:
|
|
392
|
-
- session_id: New session UUID
|
|
393
|
-
- output_file: Path to new session file
|
|
398
|
+
- session_id: New session UUID (None if nothing_to_trim)
|
|
399
|
+
- output_file: Path to new session file (None if nothing_to_trim)
|
|
394
400
|
- num_tools_trimmed: Number of tool results trimmed
|
|
395
401
|
- num_assistant_trimmed: Number of assistant messages trimmed
|
|
396
402
|
- chars_saved: Characters saved
|
|
397
403
|
- tokens_saved: Estimated tokens saved
|
|
398
404
|
- detected_agent: Detected agent type
|
|
405
|
+
- nothing_to_trim: True if savings below min_token_savings threshold
|
|
399
406
|
"""
|
|
400
407
|
import json
|
|
401
408
|
from datetime import datetime, timezone
|
|
@@ -471,6 +478,9 @@ def trim_and_create_session(
|
|
|
471
478
|
try:
|
|
472
479
|
# Parse first line and add metadata fields
|
|
473
480
|
first_line_data = json.loads(lines[0])
|
|
481
|
+
# Remove continue_metadata if present - a session is either trimmed OR
|
|
482
|
+
# continued, not both. The trim_metadata.parent_file preserves ancestry.
|
|
483
|
+
first_line_data.pop("continue_metadata", None)
|
|
474
484
|
first_line_data.update(metadata_fields)
|
|
475
485
|
lines[0] = json.dumps(first_line_data) + "\n"
|
|
476
486
|
|
|
@@ -481,6 +491,21 @@ def trim_and_create_session(
|
|
|
481
491
|
# If first line is not valid JSON, leave file as-is
|
|
482
492
|
pass
|
|
483
493
|
|
|
494
|
+
# Check if savings are worth it
|
|
495
|
+
if tokens_saved < min_token_savings:
|
|
496
|
+
# Not worth trimming - delete output file and return nothing_to_trim flag
|
|
497
|
+
output_path.unlink(missing_ok=True)
|
|
498
|
+
return {
|
|
499
|
+
"session_id": None,
|
|
500
|
+
"output_file": None,
|
|
501
|
+
"num_tools_trimmed": num_tools_trimmed,
|
|
502
|
+
"num_assistant_trimmed": num_assistant_trimmed,
|
|
503
|
+
"chars_saved": chars_saved,
|
|
504
|
+
"tokens_saved": tokens_saved,
|
|
505
|
+
"detected_agent": agent,
|
|
506
|
+
"nothing_to_trim": True,
|
|
507
|
+
}
|
|
508
|
+
|
|
484
509
|
# Inject parent session lineage into first user message
|
|
485
510
|
inject_lineage_into_first_user_message(output_path, input_file, agent)
|
|
486
511
|
|
|
@@ -492,6 +517,7 @@ def trim_and_create_session(
|
|
|
492
517
|
"chars_saved": chars_saved,
|
|
493
518
|
"tokens_saved": tokens_saved,
|
|
494
519
|
"detected_agent": agent,
|
|
520
|
+
"nothing_to_trim": False,
|
|
495
521
|
}
|
|
496
522
|
|
|
497
523
|
|