SOURCE EXPLORER / EVIDENCE BROWSER
recover-session-usage.py
Inspect the implementation and evidence, line by line.
Nichijoyaku was created using AI. A complete review by human biblical-language and biblical-studies specialists has not been established. Read alongside established translations and original-language resources.
1"""Aggregate a project-scoped Claude session archive without exporting messages."""2import argparse3import hashlib4import json5from collections import Counter6from pathlib import Path78FIELDS = ('input_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens', 'output_tokens')91011def summarize(directory):12requests = {}13errors = set()14duplicate_records = 015synthetic_records = 016inventory = hashlib.sha256()17files = sorted(directory.rglob('*.jsonl'))18for path in files:19raw = path.read_bytes()20inventory.update(str(path.relative_to(directory)).encode() + b'\0' + hashlib.sha256(raw).digest())21for line in raw.splitlines():22record = json.loads(line)23if record.get('type') != 'assistant':24continue25message = record['message']26if record.get('isApiErrorMessage') and record.get('error') == 'rate_limit':27errors.add(record['uuid'])28if message.get('model') == '<synthetic>':29synthetic_records += 130continue31if not message.get('usage'):32continue33key = message['id']34if not key or not record.get('requestId'):35raise ValueError('Usage record lacks stable message/request identity')36usage = {field: message['usage'][field] for field in FIELDS}37if any(not isinstance(n, int) or n < 0 for n in usage.values()):38raise ValueError('Invalid token counter')39item = {'request_id': record['requestId'], 'model': message['model'], 'tokens': usage}40if key in requests:41if requests[key]['identity'] != item:42raise ValueError('Conflicting usage snapshots; reconcile before publishing')43duplicate_records += 144requests[key]['timestamp'] = min(requests[key]['timestamp'], record['timestamp'])45else:46requests[key] = {'identity': item, 'timestamp': record['timestamp']}47models = {}48for request in requests.values():49item = request['identity']50model = item['model']51if model not in models:52models[model] = {'tokens': Counter(), 'requests': 0, 'first_timestamp': request['timestamp'], 'last_timestamp': request['timestamp']}53row = models[model]54row['tokens'].update(item['tokens'])55row['requests'] += 156row['first_timestamp'] = min(row['first_timestamp'], request['timestamp'])57row['last_timestamp'] = max(row['last_timestamp'], request['timestamp'])58for row in models.values():59row['total_tokens'] = sum(row['tokens'].values())60return {'schema': 1, 'captured_at': '2026-10-03',61'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.',62'archive_files': len(files), 'archive_inventory_sha256': inventory.hexdigest(),63'unique_requests': len(requests), 'duplicate_usage_records_removed': duplicate_records,64'synthetic_records_excluded': synthetic_records, 'distinct_rate_limit_error_records': len(errors),65'models': models, 'total_tokens': sum(row['total_tokens'] for row in models.values()),66'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.',67'limits': ['Only the recovered local archive is covered; other dates, machines, deleted logs and unrecorded calls are absent.',68'Haiku records include CLI session-labeling work; this is not evidence that every Haiku call translated or reviewed scripture.',69'Rate-limit error entries are not distinct quota-exhaustion episodes and carry no measured consumed-token estimate.',70'Cache token counts and nominal token totals are not a measurement of subscription quota consumed.',71'The separate 38-call release audit is not included in this total.'],72'whole_project_total_tokens': None}737475if __name__ == '__main__':76parser = argparse.ArgumentParser()77parser.add_argument('--sessions', type=Path, required=True)78print(json.dumps(summarize(parser.parse_args().sessions), ensure_ascii=False, indent=2))79