"""Aggregate a project-scoped Claude session archive without exporting messages.""" import argparse import hashlib import json from collections import Counter from pathlib import Path FIELDS = ('input_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens', 'output_tokens') def summarize(directory): requests = {} errors = set() duplicate_records = 0 synthetic_records = 0 inventory = hashlib.sha256() files = sorted(directory.rglob('*.jsonl')) for path in files: raw = path.read_bytes() inventory.update(str(path.relative_to(directory)).encode() + b'\0' + hashlib.sha256(raw).digest()) for line in raw.splitlines(): record = json.loads(line) if record.get('type') != 'assistant': continue message = record['message'] if record.get('isApiErrorMessage') and record.get('error') == 'rate_limit': errors.add(record['uuid']) if message.get('model') == '': synthetic_records += 1 continue if not message.get('usage'): continue key = message['id'] if not key or not record.get('requestId'): raise ValueError('Usage record lacks stable message/request identity') usage = {field: message['usage'][field] for field in FIELDS} if any(not isinstance(n, int) or n < 0 for n in usage.values()): raise ValueError('Invalid token counter') item = {'request_id': record['requestId'], 'model': message['model'], 'tokens': usage} if key in requests: if requests[key]['identity'] != item: raise ValueError('Conflicting usage snapshots; reconcile before publishing') duplicate_records += 1 requests[key]['timestamp'] = min(requests[key]['timestamp'], record['timestamp']) else: requests[key] = {'identity': item, 'timestamp': record['timestamp']} models = {} for request in requests.values(): item = request['identity'] model = item['model'] if model not in models: models[model] = {'tokens': Counter(), 'requests': 0, 'first_timestamp': request['timestamp'], 'last_timestamp': request['timestamp']} row = models[model] row['tokens'].update(item['tokens']) row['requests'] += 1 row['first_timestamp'] = min(row['first_timestamp'], request['timestamp']) row['last_timestamp'] = max(row['last_timestamp'], request['timestamp']) for row in models.values(): row['total_tokens'] = sum(row['tokens'].values()) return {'schema': 1, 'captured_at': '2026-10-03', 'scope': 'Local Claude session archive for the historical cli-tool/translate-bible project directory only. Not a complete project history or a provider quota statement.', 'archive_files': len(files), 'archive_inventory_sha256': inventory.hexdigest(), 'unique_requests': len(requests), 'duplicate_usage_records_removed': duplicate_records, 'synthetic_records_excluded': synthetic_records, 'distinct_rate_limit_error_records': len(errors), 'models': models, 'total_tokens': sum(row['total_tokens'] for row in models.values()), 'counting': 'Deduplicate by assistant message ID; require identical request ID, model and top-level counters across repeated records. Sum input + cache creation + cache read + output once. Do not add nested iterations, thinking details, or modelUsage.', 'limits': ['Only the recovered local archive is covered; other dates, machines, deleted logs and unrecorded calls are absent.', 'Haiku records include CLI session-labeling work; this is not evidence that every Haiku call translated or reviewed scripture.', 'Rate-limit error entries are not distinct quota-exhaustion episodes and carry no measured consumed-token estimate.', 'Cache token counts and nominal token totals are not a measurement of subscription quota consumed.', 'The separate 38-call release audit is not included in this total.'], 'whole_project_total_tokens': None} if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--sessions', type=Path, required=True) print(json.dumps(summarize(parser.parse_args().sessions), ensure_ascii=False, indent=2))