Практика эксплуатации · Продвинутый уровень
Как проверить скрытые напоминания и расход контекста в логах Claude Code
Терминальный интерфейс Claude Code показывает диалог, но не обязан отображать каждую служебную вставку, которую получила модель. Поэтому частоту ip_reminder и её возможную связь с ростом контекста нельзя надёжно оценить по экрану. Проверяем локальный JSONL-транскрипт, отдельно считаем напоминания и четыре категории токенов, а затем формируем отчёт, который можно повторно проверить без отправки истории во внешний сервис.
Что получится в конце
Мы выберем один файл сессии Claude Code и получим два локальных отчёта:
- число записей и точных открывающих тегов
<ip_reminder>; - номера строк, время, роль и путь внутри JSON для каждого совпадения;
- количество уникальных ответов модели с данными
usage; - суммы
input_tokens,output_tokens,cache_creation_input_tokensиcache_read_input_tokens; - распределение токенов по модели и по ответам;
- предупреждения о битых строках, отсутствующих идентификаторах сообщений и неоднозначных дубликатах.
Наблюдаемость здесь означает не просмотр красивого интерфейса, а возможность восстановить фактическую структуру сессии по сохранённым событиям. Контекст — всё, что модель получает перед генерацией ответа: инструкции, сообщения, результаты инструментов и служебные добавления. Токен — единица текста, используемая моделью при обработке ввода и создании вывода.
Конкретный случай: экран не объясняет рост ввода
Представим воспроизводимую ситуацию без заранее заданного результата. Разработчик ведёт длинную сессию: просит проанализировать файлы, несколько раз уточняет задачу и получает ответы обычной длины. В терминале нет отдельной строки про ip_reminder, но индикатор доступного контекста уменьшается быстрее ожидаемого.
Из интерфейса нельзя строго ответить на четыре вопроса:
- присутствовал ли маркер
<ip_reminder>в сохранённой сессии; - сколько раз он встретился и в каких записях;
- какой объём пришёлся на обычный ввод, вывод, создание кэша и чтение кэша;
- совпали ли напоминания по времени с более тяжёлыми ответами модели.
Для ответа нужен не новый эксперимент с намеренной провокацией фильтров, а аудит уже существующей сессии. Это безопаснее и методологически чище: мы не меняем исследуемый диалог и не пытаемся обойти ограничения модели.
Как устроены локальные данные
Claude Code обычно сохраняет проектные транскрипты в каталоге:
~/.claude/projects/<кодированный-путь-проекта>/<session-id>.jsonl
JSONL, или JSON Lines, — формат, в котором каждая строка является самостоятельным JSON-объектом. Одна сессия может включать пользовательские и ассистентские сообщения, системные события, результаты инструментов и вспомогательные записи.
Формат локального транскрипта является внутренней поверхностью продукта и может меняться между версиями Claude Code. Поэтому скрипт ниже не требует фиксированного набора типов строк. Он рекурсивно ищет строковые значения и объект usage, а неизвестные структуры не объявляет ошибочными автоматически.
В ответах модели могут встречаться поля:
{
"message": {
"id": "идентификатор-ответа",
"role": "assistant",
"model": "имя-модели",
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0
}
}
}
Нули показаны только как схема. Они не являются результатом теста. В конкретной версии часть полей может отсутствовать, находиться в другом месте или появляться в нескольких строках одного потокового ответа.
Что именно считать
Вхождения напоминания
Поиск простого слова ip_reminder даёт ложные совпадения: пользователь мог сам написать это имя, оно могло попасть в команду или в статью. Основной показатель скрипта — число точных открывающих тегов по регулярному выражению:
<ip_reminder(?:\s[^>]*)?>
Дополнительно отчёт сохраняет JSON-путь до совпавшего значения. Это позволяет отличить содержимое сообщения от имени поля или другого метаданного. Закрывающий тег отдельно не считается событием.
Четыре категории токенов
input_tokens- Входные токены, указанные в объекте
usage. Это не обязательно полный физический размер всего видимого пользователю диалога. output_tokens- Токены, созданные моделью для ответа.
cache_creation_input_tokens- Вход, записанный в кэш промпта. Название отражает категорию учёта, а не количество уникальных символов на диске.
cache_read_input_tokens- Вход, повторно использованный из кэша промпта.
Промпт в этой схеме шире последней пользовательской реплики: модель может получать историю и служебные инструкции. Кэшированные категории нужно показывать отдельно. Складывать их в безымянное поле «все токены», а затем трактовать сумму как размер текущего контекстного окна некорректно.
Шаг 1. Зафиксируйте среду и не меняйте исходник
Сначала запишите версию Claude Code и убедитесь, что каталог существует:
claude --version
test -d "$HOME/.claude/projects" && echo "Каталог найден"
Найдите недавние транскрипты. Команда только читает метаданные файлов:
find "$HOME/.claude/projects" \
-type f -name '*.jsonl' \
-printf '%T@ %TY-%Tm-%Td %TH:%TM:%TS %p\n' 2>/dev/null \
| sort -nr \
| head -20
В macOS системный find обычно не поддерживает -printf. Используйте:
find "$HOME/.claude/projects" -type f -name '*.jsonl' -exec stat \
-f '%m %Sm %N' -t '%Y-%m-%d %H:%M:%S' {} \; \
| sort -nr \
| head -20
Выберите файл по времени и проектному каталогу. Не используйте автоматически «самый новый», если одновременно открыты несколько сессий или работают дочерние агенты.
Задайте явный путь и проверьте его:
SESSION_FILE="/абсолютный/путь/к/session-id.jsonl"
test -f "$SESSION_FILE" || {
echo "Файл сессии не найден" >&2
exit 1
}
wc -l -c "$SESSION_FILE"
file "$SESSION_FILE"
Шаг 2. Выполните быструю разведку
До запуска полноценного анализатора полезно понять, есть ли точный маркер:
grep -n -F '<ip_reminder' "$SESSION_FILE"
Если установлен rg:
rg -n --fixed-strings '<ip_reminder' "$SESSION_FILE"
Отсутствие вывода означает только, что буквальная последовательность не найдена. Это не доказывает отсутствие любых скрытых инструкций: они могли не сохраняться, называться иначе или быть представлены другой структурой.
Не публикуйте найденные строки целиком. Транскрипт может содержать исходный запрос, фрагменты файлов, пути, результаты инструментов, персональные данные и секреты. Итоговый анализатор выводит путь и короткий обезличенный фрагмент вокруг тега, но не печатает всё сообщение.
Шаг 3. Сохраните локальный анализатор
Создайте файл audit_claude_session.py в каталоге, доступном только вам. Скрипт использует стандартную библиотеку Python, ничего не отправляет в сеть и не изменяет транскрипт.
#!/usr/bin/env python3
import argparse
import hashlib
import json
import re
import sys
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterator
TOKEN_FIELDS = (
"input_tokens",
"output_tokens",
"cache_creation_input_tokens",
"cache_read_input_tokens",
)
IP_TAG = re.compile(r"<ip_reminder(?:\s[^>]*)?>", re.IGNORECASE)
def walk(value: Any, path: str = "$") -> Iterator[tuple[str, Any]]:
yield path, value
if isinstance(value, dict):
for key, child in value.items():
yield from walk(child, f"{path}.{key}")
elif isinstance(value, list):
for index, child in enumerate(value):
yield from walk(child, f"{path}[{index}]")
def as_nonnegative_int(value: Any) -> int | None:
if isinstance(value, bool):
return None
if isinstance(value, int) and value >= 0:
return value
return None
def usage_score(usage: dict[str, int]) -> int:
return sum(usage.get(field, 0) for field in TOKEN_FIELDS)
def safe_excerpt(text: str, match: re.Match[str], radius: int = 32) -> str:
left = max(0, match.start() - radius)
right = min(len(text), match.end() + radius)
excerpt = text[left:right]
excerpt = re.sub(r"\s+", " ", excerpt)
excerpt = excerpt.replace("\n", " ")
return excerpt
def extract_usage_candidates(
record: Any,
line_number: int,
) -> list[dict[str, Any]]:
candidates = []
for path, value in walk(record):
if not isinstance(value, dict):
continue
raw_usage = value.get("usage")
if not isinstance(raw_usage, dict):
continue
usage = {}
present_fields = []
for field in TOKEN_FIELDS:
parsed = as_nonnegative_int(raw_usage.get(field))
if parsed is not None:
usage[field] = parsed
present_fields.append(field)
else:
usage[field] = 0
if not present_fields:
continue
message_id = value.get("id")
model = value.get("model")
if not isinstance(message_id, str) or not message_id:
message_id = None
if not isinstance(model, str) or not model:
model = "unknown"
candidates.append({
"line": line_number,
"path": f"{path}.usage",
"message_id": message_id,
"model": model,
"usage": usage,
"present_fields": present_fields,
})
return candidates
def choose_usage_records(
candidates: list[dict[str, Any]],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
by_message_id = defaultdict(list)
without_id = []
for candidate in candidates:
if candidate["message_id"]:
by_message_id[candidate["message_id"]].append(candidate)
else:
without_id.append(candidate)
selected = []
ambiguous = []
for message_id, versions in by_message_id.items():
signatures = {
tuple(item["usage"][field] for field in TOKEN_FIELDS)
for item in versions
}
best = max(
versions,
key=lambda item: (usage_score(item["usage"]), item["line"]),
)
selected.append(best)
if len(signatures) > 1:
ambiguous.append({
"message_id": message_id,
"versions": len(versions),
"selected_line": best["line"],
"selection_rule": "largest_usage_sum_then_latest_line",
})
# Без message.id безопасно доказать дубликаты нельзя.
# Такие usage-объекты учитываются построчно и явно помечаются.
selected.extend(without_id)
selected.sort(key=lambda item: item["line"])
return selected, ambiguous
def main() -> int:
parser = argparse.ArgumentParser(
description="Локальный аудит одного JSONL-транскрипта Claude Code."
)
parser.add_argument("session", type=Path)
parser.add_argument(
"--json-out",
type=Path,
default=Path("claude-session-audit.json"),
)
args = parser.parse_args()
session = args.session.expanduser().resolve()
if not session.is_file():
print(f"Ошибка: файл не найден: {session}", file=sys.stderr)
return 2
if session.suffix.lower() != ".jsonl":
print("Ошибка: ожидается файл с расширением .jsonl", file=sys.stderr)
return 2
digest = hashlib.sha256()
line_count = 0
parsed_records = 0
invalid_lines = []
type_counts = Counter()
reminder_hits = []
usage_candidates = []
with session.open("rb") as binary:
for chunk in iter(lambda: binary.read(1024 * 1024), b""):
digest.update(chunk)
with session.open("r", encoding="utf-8", errors="replace") as source:
for line_number, raw_line in enumerate(source, start=1):
line_count += 1
stripped = raw_line.strip()
if not stripped:
continue
try:
record = json.loads(stripped)
except json.JSONDecodeError as error:
invalid_lines.append({
"line": line_number,
"error": error.msg,
})
continue
parsed_records += 1
if isinstance(record, dict):
record_type = record.get("type", "missing")
type_counts[str(record_type)] += 1
role = None
message = record.get("message")
if isinstance(message, dict):
candidate_role = message.get("role")
if isinstance(candidate_role, str):
role = candidate_role
else:
role = None
type_counts[type(record).__name__] += 1
for path, value in walk(record):
if not isinstance(value, str):
continue
for match in IP_TAG.finditer(value):
reminder_hits.append({
"line": line_number,
"record_type": (
record.get("type")
if isinstance(record, dict)
else type(record).__name__
),
"role": role,
"json_path": path,
"excerpt": safe_excerpt(value, match),
})
usage_candidates.extend(
extract_usage_candidates(record, line_number)
)
selected_usage, ambiguous = choose_usage_records(usage_candidates)
totals = {field: 0 for field in TOKEN_FIELDS}
by_model = defaultdict(lambda: {field: 0 for field in TOKEN_FIELDS})
usage_rows = []
for item in selected_usage:
for field in TOKEN_FIELDS:
value = item["usage"][field]
totals[field] += value
by_model[item["model"]][field] += value
usage_rows.append({
"line": item["line"],
"message_id": item["message_id"],
"model": item["model"],
**item["usage"],
"present_fields": item["present_fields"],
})
cache_total = (
totals["cache_creation_input_tokens"]
+ totals["cache_read_input_tokens"]
)
reported_total = sum(totals.values())
report = {
"report_version": 1,
"generated_at_utc": datetime.now(timezone.utc).isoformat(),
"source": {
"path": str(session),
"sha256": digest.hexdigest(),
"bytes": session.stat().st_size,
"lines": line_count,
"parsed_records": parsed_records,
"invalid_json_lines": invalid_lines,
},
"ip_reminder": {
"match_rule": "opening tag <ip_reminder...>, case-insensitive",
"occurrences": len(reminder_hits),
"records_with_match": len({
hit["line"] for hit in reminder_hits
}),
"hits": reminder_hits,
},
"usage": {
"candidate_usage_objects": len(usage_candidates),
"selected_usage_records": len(selected_usage),
"records_without_message_id": sum(
1 for item in selected_usage
if item["message_id"] is None
),
"deduplication": (
"For equal message.id choose largest sum of the four "
"token fields, then latest line. Records without id "
"are counted per line."
),
"ambiguous_message_versions": ambiguous,
"totals": {
**totals,
"cache_total": cache_total,
"reported_total_all_categories": reported_total,
},
"by_model": dict(sorted(by_model.items())),
"records": usage_rows,
},
"record_types": dict(type_counts.most_common()),
"interpretation_limits": [
"A marker proves presence in the saved transcript, not its cause.",
"Token fields are provider-reported accounting categories.",
"The sum of categories is not the current context-window size.",
"Local transcript schema may change between Claude Code versions.",
"Missing fields are reported as absent and contribute zero.",
],
}
output = args.json_out.expanduser().resolve()
output.write_text(
json.dumps(report, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print("CLAUDE CODE SESSION AUDIT")
print(f"Source: {session}")
print(f"SHA-256: {report['source']['sha256']}")
print(f"JSONL lines: {line_count}")
print(f"Parsed records: {parsed_records}")
print(f"Invalid JSON lines: {len(invalid_lines)}")
print()
print("IP REMINDER")
print(f"Occurrences: {len(reminder_hits)}")
print(
"Records with match: "
f"{report['ip_reminder']['records_with_match']}"
)
for hit in reminder_hits:
print(
f" line={hit['line']} "
f"type={hit['record_type']} "
f"role={hit['role']} "
f"path={hit['json_path']}"
)
print()
print("TOKEN USAGE")
for field in TOKEN_FIELDS:
print(f"{field}: {totals[field]}")
print(f"cache_total: {cache_total}")
print(f"reported_total_all_categories: {reported_total}")
print(f"selected_usage_records: {len(selected_usage)}")
print(
"selected_records_without_message_id: "
f"{report['usage']['records_without_message_id']}"
)
print(
"ambiguous_message_versions: "
f"{len(ambiguous)}"
)
print()
print(f"JSON report: {output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Почему анализатор не суммирует каждую строку вслепую
Во время потоковой генерации один логический ответ модели может быть представлен несколькими записями с одинаковым message.id. Если сложить все найденные объекты usage, один ответ иногда можно посчитать несколько раз.
Скрипт применяет явную эвристику:
- группирует объекты
usageпоmessage.id; - выбирает версию с наибольшей суммой четырёх категорий;
- при равной сумме выбирает более позднюю строку;
- сохраняет предупреждение, если у одного ID были разные наборы значений;
- записи без ID считает по строкам и сообщает их количество.
Это не универсальный контракт Claude Code, а проверяемое правило анализа нестабильного формата. Если в вашей версии каждый фрагмент с одинаковым ID представляет отдельный оплачиваемый вызов, правило потребуется изменить. Поэтому JSON-отчёт сохраняет исходные номера строк и список неоднозначных групп.
Шаг 4. Запустите аудит выбранной сессии
Проверьте синтаксис скрипта:
python3 -m py_compile audit_claude_session.py
Запустите анализ с явным входом и выходом:
python3 audit_claude_session.py \
"$SESSION_FILE" \
--json-out ./claude-session-audit.json \
| tee ./claude-session-audit.txt
После выполнения рядом появятся:
claude-session-audit.txt— короткая сводка для чтения;claude-session-audit.json— детальный машиночитаемый отчёт.
Никаких ожидаемых чисел в статье нет: правильные значения зависят исключительно от выбранного локального файла. Нулевое число напоминаний является допустимым результатом, а не признаком ошибки анализатора.
Шаг 5. Прочитайте отчёт без неверных выводов
Сначала проверьте качество входа
Поля lines и parsed_records должны быть близки. Пустые строки не считаются JSON-записями. Если invalid_json_lines не пуст, посмотрите номера строк. Для активного файла причиной может быть ещё не дописанная последняя строка; для завершённого — повреждение или изменение формата.
Затем изучите совпадения
Для каждого вхождения проверьте:
line— строку транскрипта;record_typeиrole— тип записи и роль;json_path— место внутри объекта;excerpt— короткий фрагмент, подтверждающий точный тег.
Если тег найден внутри пользовательского текста, где вы сами обсуждали ip_reminder, это не следует автоматически считать служебной вставкой. Сделайте локальную адресную проверку строки, не печатая весь журнал:
LINE_NUMBER=123
sed -n "${LINE_NUMBER}p" "$SESSION_FILE" \
| python3 -m json.tool \
| less
Подставьте реальный номер из отчёта. Выйдите из less клавишей q. Не копируйте содержимое в публичный тикет без редактирования чувствительных данных.
После этого смотрите токены
reported_total_all_categories — арифметическая сумма четырёх отчётных категорий. Она удобна для сверки агрегирования, но не является ответом на вопрос «сколько токенов прямо сейчас находится в окне модели».
Интерпретация категорий:
- большой
input_tokensуказывает на значительный некэшированный вход в учёте конкретного вызова; - большой
output_tokensотражает длинную генерацию; - большой
cache_creation_input_tokensпоказывает объём входа, отнесённый к созданию кэша; - большой
cache_read_input_tokensпоказывает повторное использование кэшированного входа; - нулевое значение может означать настоящий ноль или отсутствие поля — различие видно в
present_fields.
Шаг 6. Постройте распределение по отдельным ответам
Одних сумм недостаточно: один аномальный ответ и постепенный рост сессии дают одинаковый итог. Следующая команда читает готовый локальный отчёт и выводит строки с наибольшей суммой категорий:
python3 - <<'PY'
import json
with open("claude-session-audit.json", encoding="utf-8") as source:
report = json.load(source)
fields = (
"input_tokens",
"output_tokens",
"cache_creation_input_tokens",
"cache_read_input_tokens",
)
rows = report["usage"]["records"]
for row in sorted(
rows,
key=lambda item: sum(item.get(field, 0) for field in fields),
reverse=True,
)[:20]:
total = sum(row.get(field, 0) for field in fields)
print(
row["line"],
row["model"],
total,
*(row.get(field, 0) for field in fields),
)
PY
Порядок столбцов после модели: общая сумма, обычный ввод, вывод, создание кэша и чтение кэша. Это локальная сортировка отчётных категорий, а не расчёт денежной стоимости.
Для распределения по моделям выполните:
python3 - <<'PY'
import json
with open("claude-session-audit.json", encoding="utf-8") as source:
report = json.load(source)
for model, usage in report["usage"]["by_model"].items():
print(model)
for name, value in usage.items():
print(f" {name}: {value}")
PY
Шаг 7. Сопоставьте напоминания и usage по строкам
Это исследовательская сверка, а не доказательство причинности. Она показывает ближайшую следующую запись usage после каждой строки с тегом:
python3 - <<'PY'
import json
with open("claude-session-audit.json", encoding="utf-8") as source:
report = json.load(source)
usage_rows = sorted(
report["usage"]["records"],
key=lambda row: row["line"],
)
for hit in report["ip_reminder"]["hits"]:
following = next(
(row for row in usage_rows if row["line"] >= hit["line"]),
None,
)
print({
"reminder_line": hit["line"],
"reminder_path": hit["json_path"],
"following_usage": following,
})
PY
Близость строк позволяет сформулировать проверяемый вопрос: «После записи с маркером следует ответ с такими-то отчётными категориями». Она не позволяет утверждать: «Напоминание добавило ровно N токенов». В одном вызове одновременно могут измениться история, результаты инструментов, системные инструкции, модель и состояние кэша.
Как оценивать возможное влияние на контекст
Для оценки приращения нужен контрольный дизайн, а не разность двух соседних строк. Минимально разумная схема:
- выбрать несколько уже существующих ответов с тегом и без него;
- сопоставлять вызовы одной модели и близкого этапа сессии;
- отдельно сравнивать все четыре поля
usage; - учитывать длину пользовательской реплики и объём результатов инструментов;
- не смешивать вызовы до и после компактизации истории;
- сообщать диапазон наблюдений, а не одно «точное влияние».
Даже такой анализ останется наблюдательным. Транскрипт обычно не даёт контрфактического ответа на вопрос, сколько токенов потребил бы тот же запрос в том же состоянии, но без служебной вставки. Для строгого причинного измерения потребовался бы контролируемый интерфейс формирования запроса, которого обычный локальный журнал не предоставляет.
Проверка корректности анализатора
Не доверяйте скрипту только потому, что он завершился без ошибки. Проведите четыре независимые проверки.
1. Сверьте хеш входного файла
sha256sum "$SESSION_FILE"
python3 - <<'PY'
import json
with open("claude-session-audit.json", encoding="utf-8") as source:
print(json.load(source)["source"]["sha256"])
PY
На macOS вместо sha256sum используйте shasum -a 256. Значения должны совпасть, если файл не изменился между запуском и проверкой.
2. Сверьте число тегов независимой командой
python3 - "$SESSION_FILE" <<'PY'
import re
import sys
pattern = re.compile(r"<ip_reminder(?:\s[^>]*)?>", re.I)
count = 0
with open(sys.argv[1], encoding="utf-8", errors="replace") as source:
for line in source:
count += len(pattern.findall(line))
print(count)
PY
Число должно совпасть с ip_reminder.occurrences. Эта сверка не определяет происхождение тегов, но проверяет механику подсчёта.
3. Вручную сложите небольшой фрагмент
Выберите несколько последовательных элементов из usage.records, сложите каждую категорию отдельно и сравните с независимым коротким скриптом или таблицей. Не проверяйте только общую сумму: перестановка категорий тогда останется незаметной.
4. Запустите анализ дважды
python3 audit_claude_session.py \
"$SESSION_FILE" \
--json-out ./audit-first.json >./audit-first.txt
python3 audit_claude_session.py \
"$SESSION_FILE" \
--json-out ./audit-second.json >./audit-second.txt
Поля generated_at_utc будут различаться. Остальные данные должны совпасть, если исходный файл не менялся. Для завершённой сессии сравните отчёты после исключения времени генерации:
python3 - <<'PY'
import json
def load(path):
with open(path, encoding="utf-8") as source:
value = json.load(source)
value.pop("generated_at_utc", None)
return value
assert load("audit-first.json") == load("audit-second.json")
print("Проверка пройдена")
PY
Типовые сбои и способы диагностики
- Каталог
~/.claude/projectsотсутствует - Проверьте, под тем ли пользователем запущен Claude Code, не переопределена ли домашняя директория и создавалась ли локальная сессия. Не ищите рекурсивно по всей файловой системе с повышенными правами без необходимости.
- Выбран не тот JSONL
- Сопоставьте время изменения, проектный каталог и идентификатор сессии. Файл дочернего агента или соседнего проекта может выглядеть правдоподобно, но отвечать на другой вопрос.
- Последняя строка не разбирается
- Активный процесс мог ещё дописывать запись. Завершите сессию, сделайте копию и повторите анализ. Не удаляйте и не исправляйте строку в оригинале.
ip_reminderнайден, но его написал пользователь- Проверьте роль, JSON-путь и окружение тега. Само совпадение не устанавливает автора вставки. Для отчёта разделите «совпадения в пользовательском тексте» и «совпадения в других сохранённых полях».
- Поиск словом даёт больше результатов, чем анализатор
- Обычный поиск находит название без тега, закрывающие теги и обсуждение термина. Анализатор намеренно считает только точный открывающий тег.
- Все token-поля равны нулю
- Откройте несколько строк с типом
assistantи проверьте фактическую структуру. В вашей версии данные могли отсутствовать или переместиться. Посмотритеrecord_typesи не переименовывайте поля наугад. - Число объектов
usageбольше числа ответов - Вероятны потоковые или повторные записи. Проверьте
ambiguous_message_versionsи одинаковыеmessage_id. Не суммируйте кандидаты до определения правила дедупликации. - Много записей без
message.id - Их нельзя надёжно дедуплицировать по использованной эвристике. Сверьте несколько строк вручную и при необходимости добавьте составной ключ из полей, реально присутствующих в вашей версии.
- Сумма не совпадает с интерфейсом или биллингом
- Локальный транскрипт, индикатор контекста, лимит подписки и счёт поставщика — разные источники с разной семантикой. Кэш, округление, модель, повторные запросы и политика учёта могут различаться. Не объявляйте локальную сумму финансово точной без отдельной сверки.
- Отчёт раскрывает чувствительный текст
- По умолчанию храните его локально с ограниченными правами. Перед передачей удалите путь к файлу, фрагменты сообщений, идентификаторы и любые данные, которые не нужны для воспроизведения ошибки.
Безопасное хранение отчёта
Сам отчёт менее чувствителен, чем полный транскрипт, но всё ещё содержит абсолютный путь, идентификаторы сообщений, модели, время и короткие фрагменты вокруг тега. Ограничьте права:
chmod 600 \
./claude-session-audit.json \
./claude-session-audit.txt
Если отчёт должен попасть в репозиторий, сначала создайте отдельную отредактированную копию. Не добавляйте исходный JSONL и не маскируйте секреты простой заменой одного известного ключа: чувствительные данные могут находиться в сообщениях и результатах инструментов.
Для командной работы лучше публиковать агрегаты и методику:
- версию Claude Code;
- хеш исходника без самого исходника;
- число разобранных и повреждённых строк;
- правило поиска тега;
- правило дедупликации;
- суммы категорий без текстов сообщений;
- явные ограничения интерпретации.
Ограничения метода
- Локальный журнал не равен точному сетевому запросу. Некоторые служебные данные могут не сохраняться или сохраняться в преобразованном виде.
- Схема JSONL не считается стабильным аналитическим контрактом. После обновления Claude Code проверяйте типы записей и пути полей заново.
- Тег не раскрывает механизм срабатывания. По одному транскрипту нельзя надёжно восстановить классификатор или условие, которое привело к вставке.
- Корреляция не является влиянием. Соседство напоминания и большого
usageне доказывает, что именно напоминание создало рост. - Кэшированные токены не следует трактовать как новые уникальные данные. Это отдельные категории учёта повторно используемого входа.
- Сумма категорий не равна заполнению окна контекста. Компактизация, повторное использование префикса и внутренняя сборка запроса требуют отдельного анализа.
- Отчёт не рассчитывает стоимость. Для этого нужны актуальные правила тарификации конкретной модели и среды доступа.
- Нулевой результат не доказывает глобальное отсутствие механизма. Он относится только к выбранному файлу, регулярному выражению и моменту проверки.
Критерии завершённого аудита
Работу можно считать повторяемой, если выполнены все пункты:
- выбран один явный JSONL-файл сессии;
- записана версия Claude Code;
- сохранён SHA-256 исходного файла;
- исходник не изменялся анализатором;
- отдельно посчитаны открывающие теги
ip_reminderи записи с совпадениями; - для каждого совпадения известны строка, роль и JSON-путь;
- четыре категории токенов не смешаны между собой;
- описано правило дедупликации потоковых записей;
- проверены отсутствующие ID и неоднозначные версии
usage; - результат независимо сверен хотя бы двумя командами;
- выводы не приписывают напоминанию причинное влияние без контрольного сравнения;
- отчёт хранится локально и не раскрывает полный транскрипт.
Главный результат такого аудита — не сенсационное число, а проверяемая граница знания. Вы сможете точно сказать, сколько маркеров обнаружено в конкретном сохранённом файле и как в нём распределены заявленные категории токенов. Всё, что касается причины вставки, фактического сетевого промпта и влияния на поведение модели, останется отдельной гипотезой до появления дополнительных данных.
Что изучить дальше
Другие воспроизводимые методы анализа агентов собраны в разделе «Руководства». Определения контекста, токенов, промптов и наблюдаемости доступны в глоссарии Agent Lab Journal.