trimtok 4.1.0__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.
trimtok/__init__.py ADDED
@@ -0,0 +1,838 @@
1
+ """trimtok Hermes plugin adapter and Python package root. Glue over core/ccr/dedup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import threading
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from .core import VERSION, compress_terminal
14
+ from .ccr import (
15
+ default_dir,
16
+ get as ccr_get,
17
+ purge as ccr_purge,
18
+ put as ccr_put,
19
+ stats as ccr_stats,
20
+ resolve_hash,
21
+ )
22
+ from .dedup import (
23
+ clear_session,
24
+ compress_tool_result,
25
+ expire_old_turns,
26
+ mask_old_turns,
27
+ note_turn,
28
+ remember,
29
+ )
30
+ from .delta_engine import clear_session as delta_clear_session
31
+ from .signals import (
32
+ Deadline,
33
+ VERBATIM_TOOLS,
34
+ estimate_tokens,
35
+ is_important,
36
+ )
37
+ from .pruners import (
38
+ group_search_hits,
39
+ prune_traceback,
40
+ prune_unified_diff,
41
+ skeletonize_code,
42
+ )
43
+ from .toon_codec import fold_json_to_toon
44
+
45
+ _MAX_GREP_PATTERN_CHARS = 256
46
+ _MAX_GREP_INPUT_CHARS = 1_000_000
47
+ _MAX_GREP_MATCHES = 1000
48
+ _MAX_GREP_OUTPUT_CHARS = 100_000
49
+ _MAX_AUDIT_BYTES = 1_048_576
50
+ _MAX_USAGE_BYTES = 1_048_576
51
+ _AUDIT_LOCK = threading.Lock()
52
+
53
+ RETRIEVE_SCHEMA = {
54
+ "name": "trimtok_retrieve",
55
+ "description": (
56
+ "Retrieve the uncompressed original content of a previously folded or windowed "
57
+ "tool result by its content reference: the short '#ref' shown in [tt:...] markers, "
58
+ "or a legacy 12-char hash. Supports optional line pagination (offset/limit), "
59
+ "focused section slicing (errors/diff/summary/tail), "
60
+ "and grep regex filtering to inspect specific details without flooding context."
61
+ ),
62
+ "parameters": {
63
+ "type": "object",
64
+ "properties": {
65
+ "hash": {
66
+ "type": "string",
67
+ "description": "The content ref shown in the [tt:... #ref] marker (e.g. '#c1'), or a legacy hex hash.",
68
+ },
69
+ "section": {
70
+ "type": "string",
71
+ "enum": ["errors", "diff", "summary", "tail"],
72
+ "description": (
73
+ "Optional focused section to retrieve: 'errors' (error lines ± context), "
74
+ "'diff' (git/unified diff hunks ± context), 'summary' (stats + head/tail + top errors), "
75
+ "or 'tail' (last N lines)."
76
+ ),
77
+ },
78
+ "context_lines": {
79
+ "type": "integer",
80
+ "description": (
81
+ "Optional context lines around matched lines (0-20, default 3). "
82
+ "Used with section='errors' or 'diff'."
83
+ ),
84
+ },
85
+ "offset": {
86
+ "type": "integer",
87
+ "description": "Optional 1-indexed line number to start reading from.",
88
+ },
89
+ "limit": {
90
+ "type": "integer",
91
+ "description": "Optional maximum number of lines to return.",
92
+ },
93
+ "grep": {
94
+ "type": "string",
95
+ "description": (
96
+ "Optional case-insensitive safe regex (no groups or multiple repetitions); "
97
+ "limited to 256 pattern characters, 1 MB input, 1000 matches, and 100 KB output."
98
+ ),
99
+ },
100
+ },
101
+ "required": ["hash"],
102
+ },
103
+ }
104
+
105
+
106
+ def _audit_path() -> Path:
107
+ return default_dir() / "audit.jsonl"
108
+
109
+
110
+ def _usage_path() -> Path:
111
+ return default_dir() / "usage.jsonl"
112
+
113
+
114
+ def _usage(entry: dict, ctx: Any = None) -> None:
115
+ if ctx is not None and not _bool_cfg(ctx, "usage_enabled", True):
116
+ return
117
+ try:
118
+ path = _usage_path()
119
+ path.parent.mkdir(parents=True, exist_ok=True)
120
+ line = json.dumps(entry, ensure_ascii=False) + "\n"
121
+ if len(line.encode("utf-8")) > _MAX_USAGE_BYTES:
122
+ line = json.dumps({"event": "usage_entry_oversize"}) + "\n"
123
+ with _AUDIT_LOCK:
124
+ backup = path.with_name(path.name + ".1")
125
+ if backup.exists() and backup.stat().st_size > _MAX_USAGE_BYTES:
126
+ backup.unlink(missing_ok=True)
127
+ if path.exists() and path.stat().st_size > _MAX_USAGE_BYTES:
128
+ path.write_text(json.dumps({"event": "usage_history_truncated"}) + "\n", encoding="utf-8")
129
+ if path.exists() and path.stat().st_size + len(line.encode("utf-8")) > _MAX_USAGE_BYTES:
130
+ os.replace(path, backup)
131
+ with path.open("a", encoding="utf-8") as f:
132
+ f.write(line)
133
+ except Exception:
134
+ pass
135
+
136
+
137
+ def _terminal_shape(stats: dict) -> str:
138
+ if str(stats.get("kind") or "") == "json":
139
+ return "json"
140
+ if stats.get("partial") or stats.get("omitted_errors"):
141
+ return "err-dense"
142
+ if (stats.get("hidden_lines") or 0) > 0:
143
+ return "repeat"
144
+ if (stats.get("dropped_lines") or 0) > 0:
145
+ return "unique"
146
+ return "mixed"
147
+
148
+
149
+ def _tool_shape(stats: dict) -> str:
150
+ if (stats.get("fold_saved") or 0) > 0 or (stats.get("intra_saved") or 0) > 0:
151
+ return "repeat"
152
+ if (stats.get("headtail_saved") or 0) > 0:
153
+ return "unique-large"
154
+ if (stats.get("skeleton_saved") or 0) > 0:
155
+ return "code-ast"
156
+ if (stats.get("minify_saved") or 0) > 0 or (stats.get("toon_saved") or 0) > 0:
157
+ return "json"
158
+ if stats.get("gate") == "reverted_gate":
159
+ return "unique"
160
+ return "mixed"
161
+
162
+
163
+ def _usage_terminal(command: Any, output: Any, stats: dict, ctx: Any) -> None:
164
+ try:
165
+ out = output if isinstance(output, str) else ""
166
+ _usage({
167
+ "ts": int(time.time()),
168
+ "hook": "terminal",
169
+ "cmd": str(command or "")[:120],
170
+ "kind": stats.get("kind"),
171
+ "gate": stats.get("gate"),
172
+ "before": len(out),
173
+ "after": int(stats.get("after_chars") or 0),
174
+ "saved": int(stats.get("saved_chars") or 0),
175
+ "partial": bool(stats.get("partial", False)),
176
+ "omitted": int(stats.get("omitted_errors") or 0),
177
+ "ms": round(float(stats.get("ms", 0.0)), 2),
178
+ "hash": str(stats.get("digest") or ""),
179
+ "shape": _terminal_shape(stats),
180
+ }, ctx=ctx)
181
+ except Exception:
182
+ pass
183
+
184
+
185
+ def _usage_tool(tool_name: Any, result: Any, stats: dict, ctx: Any) -> None:
186
+ try:
187
+ res = result if isinstance(result, str) else ""
188
+ _usage({
189
+ "ts": int(time.time()),
190
+ "hook": "tool",
191
+ "tool": str(tool_name or "tool"),
192
+ "gate": stats.get("gate"),
193
+ "before": len(res),
194
+ "after": int(stats.get("after_chars") or 0),
195
+ "saved": int(stats.get("saved_chars") or 0),
196
+ "ms": round(float(stats.get("ms", 0.0)), 2),
197
+ "hash": str(stats.get("digest") or ""),
198
+ "shape": _tool_shape(stats),
199
+ "minify": int(stats.get("minify_saved") or 0),
200
+ "fold": int(stats.get("fold_saved") or 0),
201
+ "intra": int(stats.get("intra_saved") or 0),
202
+ "skeleton": int(stats.get("skeleton_saved") or 0),
203
+ }, ctx=ctx)
204
+ except Exception:
205
+ pass
206
+
207
+
208
+ def _usage_retrieve(args: dict, *, hit: bool, ctx: Any = None, **extra: Any) -> None:
209
+ try:
210
+ entry: dict = {
211
+ "ts": int(time.time()),
212
+ "hook": "retrieve",
213
+ "hash": str((args or {}).get("hash") or "")[:16],
214
+ "hit": bool(hit),
215
+ }
216
+ entry.update({k: v for k, v in extra.items() if v is not None})
217
+ _usage(entry, ctx=ctx)
218
+ except Exception:
219
+ pass
220
+
221
+
222
+ def _audit(entry: dict, ctx: Any = None) -> None:
223
+ if ctx is not None and not _bool_cfg(ctx, "audit_enabled", True):
224
+ return
225
+ try:
226
+ path = _audit_path()
227
+ path.parent.mkdir(parents=True, exist_ok=True)
228
+ line = json.dumps(entry, ensure_ascii=False) + "\n"
229
+ if len(line.encode("utf-8")) > _MAX_AUDIT_BYTES:
230
+ line = json.dumps({"event": "audit_entry_oversize"}) + "\n"
231
+ with _AUDIT_LOCK:
232
+ backup = path.with_name(path.name + ".1")
233
+ if backup.exists() and backup.stat().st_size > _MAX_AUDIT_BYTES:
234
+ backup.unlink(missing_ok=True)
235
+ if path.exists() and path.stat().st_size > _MAX_AUDIT_BYTES:
236
+ path.write_text(json.dumps({"event": "audit_history_truncated"}) + "\n", encoding="utf-8")
237
+ if path.exists() and path.stat().st_size + len(line.encode("utf-8")) > _MAX_AUDIT_BYTES:
238
+ os.replace(path, backup)
239
+ with path.open("a", encoding="utf-8") as f:
240
+ f.write(line)
241
+ except Exception:
242
+ pass
243
+
244
+
245
+ def _config_value(ctx: Any, key: str, default: Any) -> Any:
246
+ try:
247
+ return ctx.get_config(key, default)
248
+ except Exception:
249
+ return default
250
+
251
+
252
+ def _int_cfg(ctx: Any, key: str, default: int, low: int, high: int) -> int:
253
+ try:
254
+ value = int(_config_value(ctx, key, default))
255
+ except Exception:
256
+ return default
257
+ return max(low, min(high, value))
258
+
259
+
260
+ def _float_cfg(ctx: Any, key: str, default: float, low: float, high: float) -> float:
261
+ try:
262
+ value = float(_config_value(ctx, key, default))
263
+ except Exception:
264
+ return default
265
+ return max(low, min(high, value))
266
+
267
+
268
+ def _bool_cfg(ctx: Any, key: str, default: bool) -> bool:
269
+ value = _config_value(ctx, key, default)
270
+ return value if isinstance(value, bool) else default
271
+
272
+
273
+ def _list_cfg(ctx: Any, key: str) -> list[str]:
274
+ value = _config_value(ctx, key, [])
275
+ if not isinstance(value, list):
276
+ return []
277
+ return [item.strip() for item in value if isinstance(item, str) and item.strip()]
278
+
279
+
280
+ def _excluded_command(command: Any, ctx: Any) -> bool:
281
+ normalized = " ".join(str(command or "").split()).casefold()
282
+ if not normalized:
283
+ return False
284
+ for excluded in _list_cfg(ctx, "exclude_commands"):
285
+ prefix = " ".join(excluded.split()).casefold()
286
+ if normalized == prefix or normalized.startswith(prefix + " "):
287
+ return True
288
+ return False
289
+
290
+
291
+ def _excluded_tool(tool_name: Any, ctx: Any) -> bool:
292
+ normalized = str(tool_name or "").strip().casefold()
293
+ return bool(normalized) and any(
294
+ normalized == excluded.casefold() for excluded in _list_cfg(ctx, "exclude_tools")
295
+ )
296
+
297
+
298
+ def _store_original(
299
+ original: str, compressed: str, digest: str, ctx: Any
300
+ ) -> tuple[str, str] | None:
301
+ stored = ccr_put(
302
+ original,
303
+ max_entries=_int_cfg(ctx, "ccr_max_entries", 1000, 10, 100000),
304
+ max_bytes=_int_cfg(ctx, "ccr_max_bytes", 50 * 1024 * 1024, 1024, 1_000_000_000),
305
+ ttl_hours=_int_cfg(ctx, "ccr_ttl_hours", 24, 1, 24 * 30),
306
+ )
307
+ if not stored:
308
+ return None
309
+ if digest:
310
+ old_ref = f"#{digest}"
311
+ if stored != old_ref:
312
+ compressed = compressed.replace(old_ref, stored)
313
+ return compressed, stored
314
+
315
+
316
+ def _compile_grep(source: str) -> re.Pattern[str]:
317
+ if len(source) > _MAX_GREP_PATTERN_CHARS:
318
+ raise ValueError(f"pattern exceeds {_MAX_GREP_PATTERN_CHARS} characters")
319
+
320
+ repeats = 0
321
+ in_class = False
322
+ escaped = False
323
+ last_was_repeat = False
324
+ for char in source:
325
+ if escaped:
326
+ if char.isdigit():
327
+ raise ValueError("backreferences are not supported in grep patterns")
328
+ escaped = False
329
+ last_was_repeat = False
330
+ continue
331
+ if char == "\\":
332
+ escaped = True
333
+ last_was_repeat = False
334
+ continue
335
+ if in_class:
336
+ if char == "]":
337
+ in_class = False
338
+ continue
339
+ if char == "[":
340
+ in_class = True
341
+ last_was_repeat = False
342
+ continue
343
+ if char in "(){}":
344
+ raise ValueError("groups and counted repetition are not supported in grep patterns")
345
+ if char in "*+?":
346
+ if char == "?" and last_was_repeat:
347
+ last_was_repeat = False
348
+ continue
349
+ repeats += 1
350
+ if repeats > 1:
351
+ raise ValueError("only one repetition operator is supported in grep patterns")
352
+ last_was_repeat = True
353
+ else:
354
+ last_was_repeat = False
355
+ return re.compile(source, re.IGNORECASE)
356
+
357
+
358
+ def _finalize(
359
+ raw: str,
360
+ compressed: str,
361
+ stats: dict,
362
+ ctx: Any,
363
+ audit_base: dict,
364
+ ) -> str | None:
365
+ if stats.get("gate") != "ok":
366
+ return None
367
+ stored = _store_original(raw, compressed, str(stats.get("digest", "")), ctx)
368
+ if stored is None:
369
+ return None
370
+ new_text, digest = stored
371
+ _audit({
372
+ **audit_base,
373
+ "before_chars": stats.get("before_chars"),
374
+ "after_chars": stats.get("after_chars"),
375
+ "saved_chars": stats.get("saved_chars"),
376
+ "gate": "ok",
377
+ "hash": digest,
378
+ "ms": round(float(stats.get("ms", 0.0)), 2),
379
+ }, ctx=ctx)
380
+ return new_text
381
+
382
+
383
+ def _on_transform(
384
+ command: Any = None,
385
+ output: Any = None,
386
+ returncode: Any = 0,
387
+ **kw: Any,
388
+ ) -> str | None:
389
+ ctx = _on_transform._ctx # type: ignore[attr-defined]
390
+ if not _bool_cfg(ctx, "enabled", True):
391
+ return None
392
+ if not isinstance(output, str):
393
+ return None
394
+ if _excluded_command(command, ctx):
395
+ return None
396
+ min_chars = _int_cfg(ctx, "min_chars", 2048, 0, 10_000_000)
397
+ new_output, stats = compress_terminal(
398
+ str(command or ""),
399
+ output,
400
+ min_chars=min_chars,
401
+ max_lines=_int_cfg(ctx, "max_lines", 120, 10, 10000),
402
+ session_id=str(kw.get("session_id") or kw.get("task_id") or ""),
403
+ delta_enabled=_bool_cfg(ctx, "delta_enabled", True),
404
+ delta_threshold=_float_cfg(ctx, "delta_threshold", 0.65, 0.5, 1.0),
405
+ deadline=Deadline(_float_cfg(ctx, "guard_ms", 50.0, 1.0, 1000.0)),
406
+ )
407
+ _usage_terminal(command, output, stats, ctx)
408
+ return _finalize(output, new_output, stats, ctx, {
409
+ "tool": "terminal",
410
+ "command": str(command or "")[:120],
411
+ "kind": stats.get("kind"),
412
+ })
413
+
414
+
415
+ def _on_tool_result(
416
+ tool_name: Any = None,
417
+ args: Any = None,
418
+ result: Any = None,
419
+ session_id: Any = None,
420
+ task_id: Any = None,
421
+ turn_id: Any = None,
422
+ **kw: Any,
423
+ ) -> str | None:
424
+ ctx = _on_tool_result._ctx # type: ignore[attr-defined]
425
+ if not _bool_cfg(ctx, "enabled", True):
426
+ return None
427
+ if not _bool_cfg(ctx, "phase2_enabled", True):
428
+ return None
429
+ if not isinstance(result, str):
430
+ return None
431
+ if _excluded_tool(tool_name, ctx):
432
+ return None
433
+ verbatim_name = str(tool_name or "").strip()
434
+ if verbatim_name in VERBATIM_TOOLS and verbatim_name != "read_file":
435
+ return None
436
+ sid = str(session_id or task_id or "default")
437
+ turn = str(turn_id or "")
438
+ new_result, stats = compress_tool_result(
439
+ str(tool_name or ""),
440
+ result,
441
+ session_id=sid,
442
+ min_chars=_int_cfg(ctx, "min_chars", 2048, 0, 10_000_000),
443
+ toon_enabled=_bool_cfg(ctx, "toon_enabled", True),
444
+ deadline=Deadline(_float_cfg(ctx, "guard_ms", 50.0, 1.0, 1000.0)),
445
+ )
446
+ _usage_tool(tool_name, result, stats, ctx)
447
+ out = _finalize(result, new_result, stats, ctx, {
448
+ "tool": str(tool_name or "tool"),
449
+ "kind": "tool_result",
450
+ })
451
+ if out is not None:
452
+ note_turn(result, str(stats.get("digest", "")), session_id=sid, turn_id=turn)
453
+ return out
454
+
455
+
456
+ def _on_post_llm(
457
+ session_id: Any = None,
458
+ task_id: Any = None,
459
+ turn_id: Any = None,
460
+ **kw: Any,
461
+ ) -> None:
462
+ ctx = _on_post_llm._ctx # type: ignore[attr-defined]
463
+ if not _bool_cfg(ctx, "enabled", True) or not _bool_cfg(ctx, "mask_enabled", True):
464
+ return None
465
+ sid = str(session_id or task_id or "default")
466
+ try:
467
+ expired = expire_old_turns(
468
+ sid,
469
+ keep_turns=_int_cfg(ctx, "mask_keep_turns", 2, 0, 32),
470
+ )
471
+ if expired:
472
+ _audit({"tool": "mask", "expired_turns": expired, "gate": "ok", "ms": 0.0}, ctx=ctx)
473
+ except Exception:
474
+ pass
475
+ return None
476
+
477
+
478
+ def _parse_ref(raw: Any) -> str:
479
+ ref = str(raw or "").strip().strip("<>").strip()
480
+ ref = ref.removeprefix("tt:").split(",")[0].strip()
481
+ ref = ref.removeprefix("retrieve:").strip()
482
+ if ref.startswith("#"):
483
+ return "#" + re.sub(r"[^0-9A-Za-z]", "", ref[1:])[:16]
484
+ if re.match(r"^c[0-9A-Za-z]+$", ref) and len(ref) <= 8:
485
+ return "#" + ref
486
+ hex_ref = "".join(c for c in ref if c in "0123456789abcdef")[:16]
487
+ if hex_ref:
488
+ return hex_ref
489
+ return re.sub(r"[^0-9A-Za-z]", "", ref)[:16]
490
+
491
+
492
+ def _safe_int(val: Any, default: int) -> int:
493
+ try:
494
+ if val is None or isinstance(val, bool):
495
+ return default
496
+ return int(val)
497
+ except Exception:
498
+ return default
499
+
500
+
501
+ def _render_context_intervals(
502
+ lines: list[str], match_indices: list[int], ctx_lines: int
503
+ ) -> tuple[str, int]:
504
+ total_lines = len(lines)
505
+ intervals: list[tuple[int, int]] = []
506
+ for idx in match_indices:
507
+ s = max(0, idx - ctx_lines)
508
+ e = min(total_lines, idx + ctx_lines + 1)
509
+ if not intervals:
510
+ intervals.append((s, e))
511
+ else:
512
+ last_s, last_e = intervals[-1]
513
+ if s <= last_e:
514
+ intervals[-1] = (last_s, max(last_e, e))
515
+ else:
516
+ intervals.append((s, e))
517
+ chunks: list[str] = []
518
+ returned_lines = 0
519
+ for s, e in intervals:
520
+ chunk_lines = [f"{i + 1}|{lines[i]}" for i in range(s, e)]
521
+ returned_lines += len(chunk_lines)
522
+ chunks.append("\n".join(chunk_lines))
523
+ return "\n[...]\n".join(chunks), returned_lines
524
+
525
+
526
+ def _handle_retrieve(args: dict, **kw: Any) -> str:
527
+ ref = _parse_ref((args or {}).get("hash"))
528
+ if not ref:
529
+ return _err("hash is required (a '#ref' from a [tt:...] marker, or a legacy hex hash)")
530
+ ctx = getattr(_handle_retrieve, "_ctx", None)
531
+ ttl_hours = _int_cfg(ctx, "ccr_ttl_hours", 24, 1, 24 * 30) if ctx is not None else 24
532
+ content = ccr_get(ref, ttl_hours=ttl_hours)
533
+ if content is None:
534
+ _usage_retrieve(args, hit=False, ctx=ctx)
535
+ return _err("Not found or expired (TTL passed). Re-run the original command instead.")
536
+
537
+ grep = str((args or {}).get("grep") or "").strip()
538
+ if grep:
539
+ try:
540
+ if len(content) > _MAX_GREP_INPUT_CHARS:
541
+ return _err(
542
+ f"Content exceeds the {_MAX_GREP_INPUT_CHARS}-character grep limit; retrieve a smaller slice."
543
+ )
544
+ pattern = _compile_grep(grep)
545
+ lines = content.splitlines()
546
+ total_lines = len(lines)
547
+ matched: list[str] = []
548
+ output_chars = 0
549
+ matches_count = 0
550
+ truncated = False
551
+ for i, line in enumerate(lines):
552
+ if not pattern.search(line):
553
+ continue
554
+ matches_count += 1
555
+ rendered = f"{i + 1}|{line}"
556
+ extra = len(rendered) + (1 if matched else 0)
557
+ if len(matched) >= _MAX_GREP_MATCHES or output_chars + extra > _MAX_GREP_OUTPUT_CHARS:
558
+ truncated = True
559
+ continue
560
+ matched.append(rendered)
561
+ output_chars += extra
562
+ _usage_retrieve(args, hit=True, ctx=ctx, mode="grep",
563
+ matches=matches_count, truncated=truncated)
564
+ return _ok({
565
+ "matches": "\n".join(matched) if matched else "(no matches found)",
566
+ "matches_count": matches_count,
567
+ "total_lines": total_lines,
568
+ "truncated": truncated,
569
+ })
570
+ except Exception as e:
571
+ return _err(f"Invalid grep pattern: {e}")
572
+
573
+ lines = content.splitlines()
574
+ total_lines = len(lines)
575
+
576
+ section = str((args or {}).get("section") or "").strip().lower()
577
+ if section:
578
+ ctx_lines = max(0, min(20, _safe_int((args or {}).get("context_lines"), 3)))
579
+ if section == "errors":
580
+ match_indices = [i for i, line in enumerate(lines) if is_important(line)]
581
+ if not match_indices:
582
+ _usage_retrieve(args, hit=True, ctx=ctx, mode="section_errors", matches=0)
583
+ return _ok({
584
+ "section": "errors",
585
+ "error_matches": 0,
586
+ "total_lines": total_lines,
587
+ "lines": "(no error lines detected)",
588
+ })
589
+ formatted, returned_lines = _render_context_intervals(lines, match_indices, ctx_lines)
590
+ _usage_retrieve(args, hit=True, ctx=ctx, mode="section_errors",
591
+ matches=len(match_indices), returned=returned_lines, total=total_lines)
592
+ return _ok({
593
+ "section": "errors",
594
+ "error_matches": len(match_indices),
595
+ "returned_lines": returned_lines,
596
+ "total_lines": total_lines,
597
+ "lines": formatted,
598
+ })
599
+ if section == "diff":
600
+ diff_indices = []
601
+ diff_prefixes = ("diff --git ", "--- ", "+++ ", "index ", "similarity index ", "new file mode ", "deleted file mode ")
602
+ for i, line in enumerate(lines):
603
+ if line.startswith(diff_prefixes) or line.startswith("@@ "):
604
+ diff_indices.append(i)
605
+ elif (line.startswith("+") and not line.startswith("+++")) or (line.startswith("-") and not line.startswith("---")):
606
+ diff_indices.append(i)
607
+ if not diff_indices:
608
+ _usage_retrieve(args, hit=True, ctx=ctx, mode="section_diff", matches=0)
609
+ return _ok({
610
+ "section": "diff",
611
+ "diff_matches": 0,
612
+ "total_lines": total_lines,
613
+ "lines": "(no diff hunks or changes detected)",
614
+ })
615
+ formatted, returned_lines = _render_context_intervals(lines, diff_indices, ctx_lines)
616
+ _usage_retrieve(args, hit=True, ctx=ctx, mode="section_diff",
617
+ matches=len(diff_indices), returned=returned_lines, total=total_lines)
618
+ return _ok({
619
+ "section": "diff",
620
+ "diff_matches": len(diff_indices),
621
+ "returned_lines": returned_lines,
622
+ "total_lines": total_lines,
623
+ "lines": formatted,
624
+ })
625
+ if section == "summary":
626
+ error_indices = [i for i, line in enumerate(lines) if is_important(line)]
627
+ head_count = min(15, total_lines)
628
+ head_lines = [f"{i + 1}|{lines[i]}" for i in range(head_count)]
629
+ tail_start = max(head_count, total_lines - 25)
630
+ tail_lines = [f"{i + 1}|{lines[i]}" for i in range(tail_start, total_lines)]
631
+ top_errors = [f"{i + 1}|{lines[i]}" for i in error_indices[:20]]
632
+ _usage_retrieve(args, hit=True, ctx=ctx, mode="section_summary",
633
+ errors=len(error_indices), total=total_lines)
634
+ return _ok({
635
+ "section": "summary",
636
+ "total_lines": total_lines,
637
+ "total_chars": len(content),
638
+ "error_lines_count": len(error_indices),
639
+ "head_lines": "\n".join(head_lines),
640
+ "tail_lines": "\n".join(tail_lines),
641
+ "top_errors": "\n".join(top_errors) if top_errors else "(none)",
642
+ })
643
+ if section == "tail":
644
+ limit = _safe_int((args or {}).get("limit"), 50)
645
+ cnt = max(1, min(1000, limit))
646
+ tail_start = max(0, total_lines - cnt)
647
+ tail_slice = [f"{tail_start + i + 1}|{line}" for i, line in enumerate(lines[tail_start:])]
648
+ _usage_retrieve(args, hit=True, ctx=ctx, mode="section_tail",
649
+ returned=len(tail_slice), total=total_lines)
650
+ return _ok({
651
+ "section": "tail",
652
+ "returned_lines": len(tail_slice),
653
+ "total_lines": total_lines,
654
+ "lines": "\n".join(tail_slice),
655
+ })
656
+ return _err(f"Invalid section '{section}'. Allowed: 'errors', 'diff', 'summary', 'tail'")
657
+
658
+ offset = (args or {}).get("offset")
659
+ limit = (args or {}).get("limit")
660
+ if offset is not None or limit is not None:
661
+ start = max(0, _safe_int(offset, 1) - 1)
662
+ cnt = _safe_int(limit, 1000)
663
+ end = min(total_lines, start + max(1, cnt))
664
+ sliced = [f"{i + 1}|{lines[i]}" for i in range(start, end)]
665
+ _usage_retrieve(args, hit=True, ctx=ctx, mode="slice",
666
+ offset=start + 1, returned=len(sliced), total=total_lines)
667
+ return _ok({
668
+ "lines": "\n".join(sliced),
669
+ "offset": start + 1,
670
+ "total_lines": total_lines,
671
+ "returned_lines": len(sliced),
672
+ })
673
+
674
+ _usage_retrieve(args, hit=True, ctx=ctx, mode="full", chars=len(content))
675
+ return _ok({"original_content": content, "original_chars": len(content), "total_lines": total_lines})
676
+
677
+
678
+ def _err(message: str) -> str:
679
+ try:
680
+ from tools.registry import tool_error # type: ignore
681
+ return tool_error(message)
682
+ except Exception:
683
+ return json.dumps({"error": message}, ensure_ascii=False)
684
+
685
+
686
+ def _ok(data: dict) -> str:
687
+ try:
688
+ from tools.registry import tool_result # type: ignore
689
+ return tool_result(data)
690
+ except Exception:
691
+ return json.dumps(data, ensure_ascii=False)
692
+
693
+
694
+ def _usage_summary() -> str:
695
+ try:
696
+ path = _usage_path()
697
+ backup = path.with_name(path.name + ".1")
698
+ rows = []
699
+ with _AUDIT_LOCK:
700
+ for f in (backup, path):
701
+ if f.exists():
702
+ for ln in f.read_text(encoding="utf-8").splitlines():
703
+ s = ln.strip()
704
+ if s.startswith("{"):
705
+ try:
706
+ rows.append(json.loads(s))
707
+ except Exception:
708
+ pass
709
+ if not rows:
710
+ return f"trimtok v{VERSION}: no usage logged yet."
711
+ n_ok = sum(1 for r in rows if r.get("gate") == "ok")
712
+ saved = sum(int(r.get("saved") or 0) for r in rows if r.get("hook") in ("terminal", "tool"))
713
+ gates: dict = {}
714
+ shapes: dict = {}
715
+ cmds: dict = {}
716
+ for r in rows:
717
+ if r.get("hook") in ("terminal", "tool"):
718
+ g = str(r.get("gate") or "?")
719
+ gates[g] = gates.get(g, 0) + 1
720
+ sh = str(r.get("shape") or "?")
721
+ shapes[sh] = shapes.get(sh, 0) + 1
722
+ c = str(r.get("cmd") or r.get("tool") or "?")[:40]
723
+ cmds[c] = [cmds.get(c, [0, 0])[0] + 1, cmds.get(c, [0, 0])[1] + int(r.get("saved") or 0)]
724
+ retr = [r for r in rows if r.get("hook") == "retrieve"]
725
+ hits = sum(1 for r in retr if r.get("hit"))
726
+ top_g = sorted(gates.items(), key=lambda kv: -kv[1])[:5]
727
+ top_s = sorted(shapes.items(), key=lambda kv: -kv[1])[:5]
728
+ top_c = sorted(cmds.items(), key=lambda kv: -kv[1][1])[:5]
729
+ out = [f"trimtok v{VERSION} usage: {len(rows)} events, {n_ok} folds, ~{saved:,} chars saved."]
730
+ out.append("gates: " + ", ".join(f"{k}={v}" for k, v in top_g))
731
+ out.append("shapes: " + ", ".join(f"{k}={v}" for k, v in top_s))
732
+ out.append("top-saved: " + "; ".join(f"{k} (~{v[1]:,}c)" for k, v in top_c))
733
+ out.append(f"retrieve: {hits}/{len(retr)} hits" if retr else "retrieve: none yet")
734
+ return "\n".join(out)
735
+ except Exception:
736
+ return f"trimtok v{VERSION}: usage unavailable."
737
+
738
+
739
+ def _handle_ts_command(raw_args: str) -> str:
740
+ arg = (raw_args or "").strip().lower()
741
+ if arg in ("", "status"):
742
+ try:
743
+ path = _audit_path()
744
+ backup = path.with_name(path.name + ".1")
745
+ lines = []
746
+ with _AUDIT_LOCK:
747
+ for audit_file in (backup, path):
748
+ if audit_file.exists():
749
+ lines.extend(audit_file.read_text(encoding="utf-8").splitlines())
750
+ saved = sum((json.loads(ln).get("saved_chars") or 0) for ln in lines if ln.strip().startswith("{"))
751
+ ctx = getattr(_handle_ts_command, "_ctx", None)
752
+ ttl_hours = _int_cfg(ctx, "ccr_ttl_hours", 24, 1, 24 * 30) if ctx is not None else 24
753
+ c_stat = ccr_stats()
754
+ return (
755
+ f"trimtok v{VERSION}: {len(lines)} transforms logged (recent window), ~{saved:,} chars saved.\n"
756
+ f"CCR Cache: {c_stat.get('entries', 0)} entries (~{c_stat.get('bytes', 0) // 1024} KB)."
757
+ )
758
+ except Exception:
759
+ return f"trimtok v{VERSION}: status unavailable."
760
+ if arg == "stats":
761
+ return _handle_ts_command("status")
762
+ if arg == "usage":
763
+ return _usage_summary()
764
+ if arg == "purge":
765
+ purged = ccr_purge()
766
+ return f"trimtok v{VERSION}: purged {purged} cached original files."
767
+ if arg.startswith("retrieve"):
768
+ h = arg.partition(" ")[2].strip()
769
+ parts = h.split()
770
+ if len(parts) >= 2 and parts[1] in ("errors", "diff", "summary", "tail"):
771
+ return _handle_retrieve({"hash": parts[0], "section": parts[1]})
772
+ return _handle_retrieve({"hash": parts[0] if parts else ""})
773
+ return "Usage: /trimtok [status|stats|usage|purge|retrieve <hash> [section]]"
774
+
775
+
776
+ def _on_session_reset(session_id: Any = None, **kw: Any) -> None:
777
+ clear_session(str(session_id or ""))
778
+ delta_clear_session(str(session_id or ""))
779
+
780
+
781
+ def compress_agent_payload(
782
+ payload: dict | str, mode: str = "tool", **kw: Any
783
+ ) -> tuple[dict | str, dict]:
784
+ try:
785
+ if isinstance(payload, str):
786
+ if mode == "terminal":
787
+ return compress_terminal(kw.get("command", ""), payload)
788
+ return compress_tool_result(
789
+ kw.get("tool_name", "agent"), payload,
790
+ session_id=str(kw.get("session_id", "")),
791
+ )
792
+ if isinstance(payload, dict):
793
+ for key in ("content", "output", "stdout", "result"):
794
+ if isinstance(payload.get(key), str):
795
+ if mode == "terminal":
796
+ new_text, stats = compress_terminal(kw.get("command", ""), payload[key])
797
+ else:
798
+ new_text, stats = compress_tool_result(
799
+ str(payload.get("tool_name") or kw.get("tool_name", "agent")),
800
+ payload[key],
801
+ session_id=str(payload.get("session_id") or kw.get("session_id", "")),
802
+ )
803
+ return {**payload, key: new_text}, stats
804
+ return payload, {"gate": "passthrough_non_str"}
805
+ except Exception:
806
+ return payload, {"gate": "passthrough_exception"}
807
+
808
+
809
+ def register(ctx: Any) -> None:
810
+ _on_transform._ctx = ctx # type: ignore[attr-defined]
811
+ _on_tool_result._ctx = ctx # type: ignore[attr-defined]
812
+ _on_post_llm._ctx = ctx # type: ignore[attr-defined]
813
+ _handle_retrieve._ctx = ctx # type: ignore[attr-defined]
814
+ _handle_ts_command._ctx = ctx # type: ignore[attr-defined]
815
+ ctx.register_hook("transform_terminal_output", _on_transform)
816
+ ctx.register_hook("transform_tool_result", _on_tool_result)
817
+ ctx.register_hook("post_llm_call", _on_post_llm)
818
+ ctx.register_hook("on_session_start", _on_session_reset)
819
+ ctx.register_hook("on_session_end", _on_session_reset)
820
+ ctx.register_tool(
821
+ name="trimtok_retrieve",
822
+ toolset="trimtok",
823
+ schema=RETRIEVE_SCHEMA,
824
+ handler=_handle_retrieve,
825
+ emoji="\U0001f9f9",
826
+ )
827
+ if hasattr(ctx, "register_command"):
828
+ ctx.register_command(
829
+ "trimtok",
830
+ _handle_ts_command,
831
+ "View trimtok token savings, status, or retrieve cached originals",
832
+ )
833
+ elif hasattr(ctx, "register_slash_command"):
834
+ ctx.register_slash_command(
835
+ name="trimtok",
836
+ description="View trimtok token savings, status, or retrieve cached originals",
837
+ handler=_handle_ts_command,
838
+ )