tokenjam 0.6.2__tar.gz → 0.6.3__tar.gz
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.
- {tokenjam-0.6.2 → tokenjam-0.6.3}/CLAUDE.md +37 -2
- {tokenjam-0.6.2 → tokenjam-0.6.3}/PKG-INFO +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/getting-started.md +4 -4
- {tokenjam-0.6.2 → tokenjam-0.6.3}/npm-wrapper/package.json +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.3}/pyproject.toml +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/package.json +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_cli.py +7 -1
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_cost_proposals_api.py +7 -5
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_db.py +67 -2
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_inbox_headline_total_api.py +15 -1
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_relearn_api.py +12 -9
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_relearn_window_api.py +26 -3
- tokenjam-0.6.3/tests/integration/test_rules_surface.py +144 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_summarize_cli.py +64 -0
- tokenjam-0.6.3/tests/unit/test_analysis_span_retention.py +566 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_analyzer_scope_isolation.py +10 -7
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_backfill_progress.py +4 -4
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cost_proposal_verbs.py +8 -6
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cost_proposals.py +54 -20
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cost_proposals_model.py +44 -17
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_deadweight_apply.py +1 -1
- tokenjam-0.6.3/tests/unit/test_delivery_kinds.py +308 -0
- tokenjam-0.6.3/tests/unit/test_doctor_spans_indexes.py +118 -0
- tokenjam-0.6.3/tests/unit/test_fix_actionability.py +271 -0
- tokenjam-0.6.3/tests/unit/test_fix_catalog.py +458 -0
- tokenjam-0.6.3/tests/unit/test_fix_grounding.py +194 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_ingest_otlp.py +21 -2
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_lens_ui_regression.py +42 -2
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_model_apply.py +4 -2
- tokenjam-0.6.3/tests/unit/test_no_fix_prose_outside_the_catalog.py +233 -0
- tokenjam-0.6.3/tests/unit/test_no_sentinel_timestamps.py +367 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_backfill_scope.py +3 -3
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_path_branch.py +2 -2
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_resend_render.py +16 -2
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_summarize.py +256 -7
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_past_overspend.py +5 -3
- tokenjam-0.6.3/tests/unit/test_quickstart.py +1449 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_relearn.py +19 -10
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_relearn_apply.py +91 -50
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_relearn_archive_and_cost.py +28 -16
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_relearn_cli.py +9 -5
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_relearn_families_and_boundary.py +7 -1
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_relearn_window.py +36 -2
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_rollup_subagent_downsize_dedup.py +63 -0
- tokenjam-0.6.3/tests/unit/test_rule_placement.py +415 -0
- tokenjam-0.6.3/tests/unit/test_rulewrite_applied_detection.py +229 -0
- tokenjam-0.6.3/tests/unit/test_rulewrite_dismissal.py +192 -0
- tokenjam-0.6.3/tests/unit/test_rulewrite_legacy.py +176 -0
- tokenjam-0.6.3/tests/unit/test_rulewrite_lifecycle.py +325 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_spans_stats_repair.py +109 -0
- tokenjam-0.6.3/tests/unit/test_subagent_rubric_contradiction.py +97 -0
- tokenjam-0.6.3/tests/unit/test_summarize_calibrate.py +199 -0
- tokenjam-0.6.3/tests/unit/test_summarize_classify.py +309 -0
- tokenjam-0.6.3/tests/unit/test_summarize_estimate.py +251 -0
- tokenjam-0.6.3/tests/unit/test_summarize_numbering.py +79 -0
- tokenjam-0.6.3/tests/unit/test_summarize_relocate.py +307 -0
- tokenjam-0.6.3/tests/unit/test_summarize_route.py +195 -0
- tokenjam-0.6.3/tests/unit/test_summarize_sections.py +90 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_summarize_session.py +131 -2
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_write_budget.py +84 -32
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/app.py +2 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/logs.py +54 -14
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/relearn.py +20 -22
- tokenjam-0.6.3/tokenjam/api/routes/rules.py +183 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/backfill_progress.py +48 -3
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_doctor.py +219 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_onboard.py +136 -8
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_optimize.py +26 -4
- tokenjam-0.6.3/tokenjam/cli/cmd_quickstart.py +716 -0
- tokenjam-0.6.3/tokenjam/cli/cmd_rules.py +383 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_summarize.py +197 -4
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cost_proposal_verbs.py +11 -12
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/main.py +7 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/relearn_write_verbs.py +11 -4
- tokenjam-0.6.3/tokenjam/core/analysis_span.py +207 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/backfill.py +30 -7
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/config.py +17 -2
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/data_span.py +32 -3
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/db.py +335 -18
- tokenjam-0.6.3/tokenjam/core/fixes/__init__.py +54 -0
- tokenjam-0.6.3/tokenjam/core/fixes/catalog.py +250 -0
- tokenjam-0.6.3/tokenjam/core/fixes/grounding.py +156 -0
- tokenjam-0.6.3/tokenjam/core/fixes/lint.py +406 -0
- tokenjam-0.6.3/tokenjam/core/fixes/registry.py +806 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/ingest_adapters/codex.py +22 -3
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/models.py +5 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/context_resend.py +52 -34
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/deadweight.py +10 -7
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/model_downgrade.py +6 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/output_verbosity.py +3 -7
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/relearn.py +151 -144
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/summarize.py +237 -18
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/workflow_restructure.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/cost_apply.py +22 -1
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/cost_proposals.py +625 -155
- tokenjam-0.6.3/tokenjam/core/optimize/dismissals.py +149 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/inbox_contribution.py +6 -3
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/model_apply.py +1 -1
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/relearn_apply.py +211 -99
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/relearn_proposals.py +2 -2
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/relearn_store.py +5 -2
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/relearn_window.py +21 -0
- tokenjam-0.6.3/tokenjam/core/optimize/rule_placement.py +490 -0
- tokenjam-0.6.3/tokenjam/core/optimize/rule_scope.py +95 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/types.py +12 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/write_budget.py +252 -44
- tokenjam-0.6.3/tokenjam/core/retention.py +91 -0
- tokenjam-0.6.3/tokenjam/core/rulewrite/__init__.py +56 -0
- tokenjam-0.6.3/tokenjam/core/rulewrite/apply.py +296 -0
- tokenjam-0.6.3/tokenjam/core/rulewrite/delivery.py +404 -0
- tokenjam-0.6.3/tokenjam/core/rulewrite/kinds.py +91 -0
- tokenjam-0.6.3/tokenjam/core/rulewrite/legacy.py +151 -0
- tokenjam-0.6.3/tokenjam/core/rulewrite/plan.py +277 -0
- tokenjam-0.6.3/tokenjam/core/rulewrite/store.py +271 -0
- tokenjam-0.6.3/tokenjam/core/rulewrite/types.py +245 -0
- tokenjam-0.6.3/tokenjam/core/summarize/calibrate.py +299 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/summarize/candidates.py +59 -3
- tokenjam-0.6.3/tokenjam/core/summarize/classify.py +480 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/summarize/delivery.py +1 -1
- tokenjam-0.6.3/tokenjam/core/summarize/detect.py +284 -0
- tokenjam-0.6.3/tokenjam/core/summarize/estimate.py +248 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/summarize/load_semantics.py +81 -6
- tokenjam-0.6.3/tokenjam/core/summarize/numbering.py +102 -0
- tokenjam-0.6.3/tokenjam/core/summarize/relocate.py +375 -0
- tokenjam-0.6.3/tokenjam/core/summarize/route.py +284 -0
- tokenjam-0.6.3/tokenjam/core/summarize/sections.py +142 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/summarize/session.py +188 -13
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/summarize/wrap.py +12 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/transcript.py +61 -1
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/otel/otlp_parsing.py +15 -4
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/ui/index.html +263 -22
- tokenjam-0.6.2/tests/unit/test_quickstart.py +0 -1100
- tokenjam-0.6.2/tests/unit/test_summarize_estimate.py +0 -101
- tokenjam-0.6.2/tokenjam/cli/cmd_quickstart.py +0 -839
- tokenjam-0.6.2/tokenjam/core/retention.py +0 -21
- tokenjam-0.6.2/tokenjam/core/summarize/detect.py +0 -111
- tokenjam-0.6.2/tokenjam/core/summarize/estimate.py +0 -124
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.dockerignore +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/CODEOWNERS +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/ISSUE_TEMPLATE/integration_request.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/actions/docker-publish/action.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/pull_request_template.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/scripts/archive_traffic.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/scripts/contributor_label_guard.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/workflows/ci.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/workflows/contributor-label-guard.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/workflows/docker-publish.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/workflows/publish-npm.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/workflows/publish-pypi.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.github/workflows/traffic-archive.yml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/.gitignore +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/AGENTS.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/CODE_OF_CONDUCT.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/CONTRIBUTING.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/Dockerfile +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/LICENSE +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/Makefile +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/SECURITY.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/agent-capability-matrix.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/alerts.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/architecture.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/assets/tj-quickstart-hero.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/assets/tokenjam-flow-band.svg +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/assets/tokenjam-token-flow.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/assets/tokenjam-waste-grid.svg +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/backfill/codex.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/backfill/helicone.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/backfill/langfuse.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/backfill/otlp.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/backfill/overview.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/brand/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/brand/tokenjam-banner.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/brand/tokenjam-banner.svg +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/brand/tokenjam-icon.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/brand/tokenjam-icon.svg +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/brand/tokenjam-repo-header.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/brand/tokenjam-wordmark.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/brand/tokenjam-wordmark.svg +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/claude-code-integration.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/cli-reference.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/configuration.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/export.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/first-hour.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/framework-support.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/harness-integration.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/installation.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/internal/close-the-loop.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/internal/lens-vendor-versions.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/internal/release-smoke-checklist.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/internal/repeat-task-codification-measurement.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/internal/specs/.gitkeep +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/internal/specs/map-board-direction.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/nemoclaw-integration.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/openclaw.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/optimize/cache.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/optimize/downsize.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/optimize/reuse.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/optimize/script.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/optimize/subagent.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/optimize/trim.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/optimize/verbosity.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/policy/overview.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/proxy/overview.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/python-sdk.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/screenshots/tj-budget.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/screenshots/tj-cost.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/screenshots/tj-dashboard-leaderboard.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/screenshots/tj-dashboard-tools.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/screenshots/tj-dashboard.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/screenshots/tj-status.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/screenshots/tj-traces.png +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/docs/typescript-sdk.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/alerts_and_drift/_shared.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/alerts_and_drift/budget_breach_demo.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/alerts_and_drift/drift_demo.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/alerts_and_drift/record_outcome_demo.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/alerts_and_drift/sensitive_actions_demo.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/multi/rag_pipeline.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/multi/research_team.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/multi/router_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/multi/sample_docs/agent_patterns.txt +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/multi/sample_docs/cost_management.txt +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/multi/sample_docs/observability.txt +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/multi/sample_docs/safety.txt +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/openclaw/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/single_framework/autogen_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/single_framework/crewai_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/single_framework/langchain_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/single_framework/langgraph_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/single_framework/llamaindex_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/single_provider/anthropic_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/single_provider/bedrock_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/single_provider/gemini_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/single_provider/litellm_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/single_provider/openai_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/examples/single_provider/openai_agents_sdk_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/growth/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/incidents/hallucination-drift/BLOG.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/incidents/hallucination-drift/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/incidents/hallucination-drift/scenario.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/incidents/retry-loop/BLOG.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/incidents/retry-loop/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/incidents/retry-loop/scenario.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/incidents/surprise-cost/BLOG.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/incidents/surprise-cost/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/incidents/surprise-cost/scenario.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/npm-wrapper/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/npm-wrapper/bin/tj.js +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/scripts/release.sh +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/examples/record-outcome.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/package-lock.json +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/src/client.test.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/src/client.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/src/index.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/src/record-outcome.test.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/src/semconv.test.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/src/semconv.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/src/span-builder.test.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/src/span-builder.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/src/types.ts +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/sdk-ts/tsconfig.json +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agent-post-release-runner.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agent-post-release-v0.4.0.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agent-pre-release-runner.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agent-pre-release-v0.4.0.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agent-pre-release-v0.4.1.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agents/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agents/email_agent_budget_breach.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agents/email_agent_drift.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agents/email_agent_loop.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agents/email_agent_normal.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agents/mock_llm.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agents/test_mock_scenarios.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/agents/test_record_outcome.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/conftest.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/e2e/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/e2e/conftest.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/e2e/test_real_llm.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/factories.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/fixtures/helicone_real_response.json +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/fixtures/langfuse_real_response.json +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/fixtures/otlp_sample.json +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_agents_and_tenants_dual_units.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_analytics.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_analyzers_off_the_request_path.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_api.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_cli_api_framing_parity.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_cli_uninstall.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_cmd_proxy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_cmd_route.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_context_serve.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_cost_attribution.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_cost_charts.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_cost_compare_serve.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_cost_framing_window.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_data_access_parity.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_demo_live.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_demos.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_first_run_roundtrip_239.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_full_pipeline.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_global_json_flag.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_litellm_enrichment.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_logs_api.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_policy_api.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_proxy_app.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_proxy_policy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_quota_audit_serve.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_sdk_config_discovery.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_storage_backend_parity.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_summarize_api.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_summarize_run_api.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/integration/test_tokenmaxx_serve.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/manual-new-release-tests.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/manual-pre-release-testing.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/rate_bands.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/results/agent-post-release-v0.4.0-20260619T221412Z.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/results/agent-pre-release-v0.4.0-20260619T202233Z.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/results/agent-pre-release-v0.4.0-20260619T205108Z.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/results/agent-pre-release-v0.4.1-20260620T002546Z.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/synthetic/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/synthetic/test_alert_rules.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/synthetic/test_async_hooks_concurrency.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/synthetic/test_cost_tracking.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/synthetic/test_drift_detection.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/synthetic/test_ingest.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/synthetic/test_request_capture.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/synthetic/test_schema_validation.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/token_aggregate_guard.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/toy_agent/toy_agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_agent_kind.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_alerts.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_analytics_metric_expr.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_analyzer_scope.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_api_backend_timeout.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_async_hooks.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_attribution_cache.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_backfill.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_backfill_codex.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_backfill_interrupted_flush.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_batch_placement.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_budget_cap_policy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_budget_group_cost.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_budget_route.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_bulk_insert_spans.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cache_efficacy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cache_recommend.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cache_root_cause.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cache_root_cause_proposals.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cache_root_cause_render.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cli_entrypoint.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cli_palette.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cmd_cost.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cmd_mcp_config_discovery.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cmd_policy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cmd_pricing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cmd_resume_brief.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cmd_serve_port_precheck.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cmd_session_story.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cmd_status_teaser.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cmd_stop.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cmd_stop_scoping.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cmd_tokenmaxx.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cmd_upgrade.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_compare.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_config.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_config_discovery_guard.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_config_secret_divergence.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_config_write_target.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_context_diagnostic.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_context_resend.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_contributor_label_guard.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cost.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cost_accounting_guards.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cost_api_recoverable_presentation.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cost_compare_framing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_cycle.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_data_access.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_db_close_backfill.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_db_helpers.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_deadweight.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_demo_env.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_demo_live.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_demo_packaging.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_demo_scenarios.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_distill.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_doctor_capture_prompts.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_doctor_first_signal.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_doctor_paths.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_doctor_statusline_wiring.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_doctor_transcript_gap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_downsize_agent_pricing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_downsize_applied_ledger_migration.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_downsize_driver_role.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_drift.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_export_claude_code.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_export_route_generators.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_formatting.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_framing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_harness_setup.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_http_exporter_auth.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_humanize.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_inbox_contribution.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_ingest_accounting.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_ingest_helicone.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_ingest_langfuse.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_lens_dashboard_states.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_lens_select_all_behaviour.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_litellm_client.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_litellm_integration.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_logs_converter.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_loop.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_loop_api.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_loop_transcript_path.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_mcp_policy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_mcp_server.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_mcp_setup_project_tj_config.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_method_capture.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_method_spine.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_model_tiers.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_models.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_mythos_premium_gates.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_no_tracked_dev_secrets.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_no_tracker_ids_in_printed_strings.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_add_project.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_budget_gate.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_capture_default.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_codex.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_daemon.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_detect.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_ephemeral_guard.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_first_run.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_hygiene.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_path_resolution.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_plan.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_relearn_activation.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_resume_hook.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_stack_detection.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_statusline.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_tool_inputs_default.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_verify.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_verify_cli.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_onboard_zshrc_otel.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_openclaw_ingest.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_analyzers_route.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_clustering.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_plan_multiplier.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_recoverable.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_reuse.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_savings_ci.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_subagent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_threshold_rendering.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_trim_provenance.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_validate.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_optimize_verbosity.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_opus_quota_audit.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_persona_analyzer_gate.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_phases.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_ping.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_plan_tier_apply.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_policy_engine.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_pricing_axes.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_pricing_coverage.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_pricing_lookup_forms.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_pricing_override.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_prompt_bloat.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_provider_attribution.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_provider_attribution_194.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_provider_content_capture.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_provider_run_id.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_proxy_audit.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_proxy_gate.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_rate_profile.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_recommendations.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_relearn_otel.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_repeat_task.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_report_roundtrip.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_reset.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_resume_brief.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_reuse_skeleton.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_rollup_covers_every_rendered_row.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_runlink.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_schema_self_heal.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_sdk_agent_session.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_sdk_attribution.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_serve_transcript_catchup.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_server_state.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_session_cost_drift.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_session_label.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_session_status_lifecycle.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_session_subagents.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_sessionmap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_span_pricing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_status_sdk.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_statusline.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_stream_usage_analyzer.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_stream_usage_capture.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_summarize_apply.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_summarize_backup.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_summarize_candidates.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_summarize_catalog.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_summarize_delivery.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_summarize_detect.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_summarize_invocations.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_summarize_load_semantics.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_summarize_repo_roots.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_summarize_wrap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_time_parse.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_transcript.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_transcript_cache.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_transcript_sync.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_transport_401.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_ui_offline.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_uninstall_persistent_install.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_uninstall_runner_detect.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_uninstall_wrapper.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_usage.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_workflow_restructure.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tests/unit/test_workmap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/deps.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/middleware.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/_body.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/agents.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/alerts.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/analytics.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/budget.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/context.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/cost.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/cost_compare.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/drift.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/loop.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/metrics.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/optimize.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/otlp.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/policy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/quota_audit.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/recommendations.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/reuse.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/runs.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/sessions.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/spans.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/status.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/summarize.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/summarize_run.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/tools.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/traces.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/api/routes/version.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/banner.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_alerts.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_backfill.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_budget.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_context.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_cost.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_demo.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_drift.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_export.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_loop.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_mcp.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_otel.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_ping.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_policy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_pricing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_proxy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_quota_audit.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_relearn.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_report.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_reset.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_resume_brief.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_route.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_serve.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_session_end.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_session_story.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_status.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_statusline.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_stop.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_tokenmaxx.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_tools.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_traces.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_uninstall.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/cmd_upgrade.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/data_access.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/entrypoint.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/home.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/json_option.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/cli/onboard_detect.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/agent_kind.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/alerts.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/api_backend.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/atomic_write.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/attribution_cache.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/context_diagnostic.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/cost.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/cycle.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/distill.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/drift.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/export/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/export/ccr.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/export/claude_code.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/export/common.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/export/litellm.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/export/reuse_report.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/export/reuse_skeleton.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/framing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/harness_setup.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/ingest.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/ingest_adapters/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/ingest_adapters/helicone.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/ingest_adapters/langfuse.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/ingest_adapters/otlp.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/loop.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/method_capture.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/method_spine.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/model_tiers.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/onboard_verify.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/README.md +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/accounting.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/batch_placement.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/budget_projection.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/cache_efficacy.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/cache_recommend.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/downsize_agents.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/plan_reuse.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/prompt_bloat.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/resend_tail.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/stream_usage.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/analyzers/subagent_rightsizing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/clustering.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/projection.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/rate_profile.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/registry.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/relearn_otel.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/repeat_task.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/report_store.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/runner.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/scope.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/span_pricing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/stats.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/optimize/validate.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/phases.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/pricing.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/pricing_coverage.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/recommendations.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/resume_brief.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/runlink.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/savings_log.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/schema_validator.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/server_state.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/session_timeline.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/sessionmap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/summarize/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/summarize/agent_files.toml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/summarize/apply.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/summarize/backup.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/summarize/catalog.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/summarize/invocations.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/summarize/repo_roots.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/transcript_cache.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/transcript_sync.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/usage.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/core/workmap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/demo/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/demo/env.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/demo/live.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/mcp/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/mcp/server.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/otel/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/otel/provider.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/otel/semconv.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/pricing/models.toml +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/proxy/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/proxy/app.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/proxy/audit.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/proxy/engine.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/proxy/gate.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/proxy/observer.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/proxy/recommend.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/proxy/server.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/proxy/stream_usage.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/proxy/wiring.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/py.typed +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/agent.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/attribution.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/bootstrap.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/client.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/http_exporter.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/_request_capture.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/anthropic.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/autogen.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/base.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/bedrock.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/crewai.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/gemini.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/langchain.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/langgraph.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/litellm.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/llamaindex.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/nemoclaw.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/openai.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/integrations/openai_agents_sdk.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/sdk/transport.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/ui/vendor/htm.js +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/ui/vendor/preact-hooks.js +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/ui/vendor/preact.js +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/ui/vendor/uplot.css +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/ui/vendor/uplot.js +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/utils/__init__.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/utils/formatting.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/utils/humanize.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/utils/ids.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/utils/signatures.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/utils/theme.py +0 -0
- {tokenjam-0.6.2 → tokenjam-0.6.3}/tokenjam/utils/time_parse.py +0 -0
|
@@ -203,6 +203,34 @@ Post-ingest hooks run after each span is written to DB:
|
|
|
203
203
|
|
|
204
204
|
**A headline figure is the CEILING, not the expectation — and a ceiling still has to be reachable.** Where the arithmetic legitimately supports a range, the number a card leads with is the top of it: the largest figure the derivation honestly bears, not a discounted guess at what a user would actually realize. This is a founder decision and it is deliberate, so do not "correct" a headline downward by applying a realization factor, an adoption rate, or any other haircut invented at the render layer. It is not, however, a licence to publish an unreachable number: a ceiling has to be attainable in *some* real world, so a slice the fix **structurally cannot touch** — spans the remedy does not reach, sessions the analyzer never examined — does not belong inside it, and shaving those out is a correctness fix rather than a haircut. One carve-out: where a card offers a fix the user is about to apply, lead with the realized figure instead, because at that point the honest question has changed from "how large could this be" to "what will this specific action do". This is a convention about which of two defensible numbers to lead with; the numbers themselves belong on the cards and in `estimate_basis`, never in this file.
|
|
205
205
|
|
|
206
|
+
**WHERE a permanent rule lands is an INPUT to whether it is offered, not a presentation of the decision.** Four analyzers (`downsize`, `resend`, `subagent`, `relearn`) share one fix shape — a block appended to a `CLAUDE.md` — and each used to offer exactly one destination, the workspace/root file. That is wrong in both directions: a root-level rule is re-sent in every session of every project, including the ones that never exhibited the problem, and it dilutes the file whose dilution is what makes rules start being ignored. Every session records its working directory (`core/transcript.first_recorded_cwd` is the one extractor; `core/summarize/repo_roots.resolve_roots` turns those into boundary-safe roots), so `core/optimize/rule_placement` can resolve which projects incurred a finding and attribute each session's share to a destination. Because the standing cost is `per-session tokens x the sessions that load the file`, confining a rule to the projects that exhibited the behaviour can flip a net-negative verdict to net-positive with no change to the finding at all. Three things fall out and are easy to get wrong. **(a) Two costs, not one.** STANDING cost follows the sessions (what the rule costs to keep, and the only figure the saving is netted against); FOOTPRINT cost follows the FILES (`per-session x destinations written`, and the only figure the write budget is spent in). A three-project split can halve the standing cost while tripling the budget spend; charging either for the other's question makes the placement decision stop being arithmetic. **(b) The growth allowance is per FILE.** `measured_agent_file_tokens` aggregates the `summarize` scan, which is right for "may this window grow the files at all" and wrong for "may this rule grow THIS file" — 10% of an aggregate dominated by a large root file can exceed a small project `CLAUDE.md` entirely, so `measured_agent_file_tokens_by_path` + `WriteBudget.destination_budget` size each destination against its own measured size. **(c) Unifying the FIX surface must not merge the CLAIMS** (Critical Rule 27): the per-session weight maps (`DowngradeFinding.driver_session_tokens`, `ResendFinding.session_weights`) are BREAKDOWNS of each analyzer's own `past_overspend_tokens`, split by the same weights on both the token and the dollar side so every destination's implied rate equals the finding's own (Critical Rule 28). A recorded cwd that no longer exists is counted and surfaced in a `coverage_note` saying the gap is what was **not analysed** (Critical Rule 30), never reconstructed and never redistributed onto the destinations that did resolve.
|
|
207
|
+
|
|
208
|
+
**A rule a card writes must ERASE the number the card found.** `past_overspend_usd` is the maximum its analyzer knows: applying the fix should remove the behaviour that produced it. If applying it would leave the number roughly where it was, that is a correctness bug, not a UX gap. The live example: `SUBAGENT_RUBRIC_INTRO` used to end with "a subagent that does little tool work and returns a short result rarely needs the premium tier" — exactly the `output_tokens`/`tool_calls` gate deleted from `analyzers/subagent_rightsizing.py` for inverting on the expensive dispatches (Critical Rule 29), so the rule told the agent to keep doing the thing the card was billing it for. `context_resend.RIGHTSIZE_FIX_TEMPLATE` carried the same sentence in a different costume. Two durable halves: **the shape-based default-down is the lever and stays**, and **a premium escape hatch must be checkable by an outside reader rather than self-assessed** — "unless the subtask genuinely needs deep reasoning" asks the dispatching agent to rate its own task's difficulty, and an agent asked that question answers yes, so the exception swallows the rule. State the exception as conditions that are either written into the dispatch or not.
|
|
209
|
+
|
|
210
|
+
**HOW a rule reaches the agent is a DELIVERY MECHANISM, and "rung 3 is free" is FALSE for a context-injecting one.** Appending markdown to a `CLAUDE.md` is one way to put guidance in front of an agent; a Claude Code hook is another, and for several analyzers a better one, because it delivers at the moment of the decision rather than at the top of a long context. `core/rulewrite/delivery.py` is the seam: a `DeliveryKind` owns its own renderer AND its own pricer, and adding a mechanism is a registration plus two functions — never an edit to the staging, diff, apply, undo or budget machinery, none of which may name a CLAUDE.md. The pricing half is the part that bites. `write_budget`'s rung ladder charges rung 3+ ZERO because a settings file is *executed*, never sent as prompt text; that is earned by a formatter or a guard hook, and it is **wrong for a `UserPromptSubmit` re-injection or a `PreToolUse` additionalContext nudge**, which are prompt text on a different SCHEDULE. Their cost is worse-behaved than a rule's, not better: an injected block lands in the conversation and is re-sent every subsequent turn, so it is not even bounded by the number of injections the way a session-start read is bounded by the session count. The ladder grades intervention STRENGTH, not delivery, so the question "does this put tokens in front of the model" is asked of the mechanism (`DeliveryKind.carries_prompt_text`) and never inferred from the rung. Caught in this repo's own first draft of the seam: a rung-3 kind flagged as carrying prompt text still priced to zero, because its pricer routed through the ladder. An unknown mechanism is REFUSED rather than falling back to markdown — a wrong write to a real file is the one failure the lifecycle exists to prevent.
|
|
211
|
+
|
|
212
|
+
**A fix the user already dealt with must lose its OFFER and keep its FIGURE.** The rule-write surface consults the apply ledgers (`cost_applied.json` / `applied_fixes.json`, via `cost_apply.applied_signatures` / `relearn_apply.applied_signatures`, matched by `cost_apply.signature_is_applied`) and withdraws the offer for a signature already applied. `past_overspend_usd`/`_tokens` are untouched, and that is Critical Rule 32 in its sharpest form: the waste genuinely happened inside the analyzed window, and the user having since fixed it does not un-spend the money. An action-availability gate establishes that WE have no remedy left to offer, never that the spend was avoidable or free — so it may never reach back and edit a past figure, and a zero there would be the exact "no action for this" / "this was free" conflation. Pin it by asserting the whole record is byte-identical with and without the ledger entry EXCEPT the offer fields; an equal-value check passes a figure that got re-derived through a zero. Two further rules that cost real correctness here: **the row stays listed** carrying an applied state rather than vanishing (a user who applies something and then sees nothing cannot tell "done" from "broken"), and **an unreadable ledger reads as "nothing applied"** — that direction can only waste attention, while the opposite hides fixes the user never made. Reuse `signature_is_applied` rather than writing a second matcher: it already resolves a legacy agent-only mark as covering the later model-qualified signatures for that agent, and a second matcher silently reopens every card it settles.
|
|
213
|
+
|
|
214
|
+
**A fix must name an action that EXISTS, for THIS user, through a key the harness actually reads.** Four shipped defects, one framing failure — applying the fix has to be able to erase the number its analyzer found. (a) The right-sizing snippet emitted `reasoning_effort:`, which Claude Code does not read; the field is `effort` (`low`/`medium`/`high`/`xhigh`/`max`). **A silently-ignored key is worse than no line**: it converts an unfixed problem into one the user believes is fixed. It was also hardcoded to `low` for a population that is by construction large and tool-heavy, so the guess recommended making the expensive work worse — a value that cannot be derived from the observation is OMITTED, never guessed, because a plausible line in a frontmatter block is indistinguishable from a measured one. (b) The subagent lane required an agent definition file to already EXIST, and every Claude Code dispatch is a built-in (`general-purpose`, `Explore`, `Plan`, `fork`) which has none — so the branch was unreachable on the dominant corpus and the claim fell through to prose. **A user or project subagent of the same name overrides the built-in and keeps its own `model`, so no-file is not no-fix: offer the CREATE.** The origin belongs in the evidence too — `model` defaults to `inherit`, so an Opus-driven session hands every dispatch an Opus worker unless pinned; that is an unset default, not a decision, which is why it goes unnoticed. Watch the shape checks around this: a lowercase-only slug predicate silently rejected `Explore` and `Plan`, the exact names the fix targets. (c) The per-agent downsize lane told Claude Code users to "redeploy or restart the agent", but there `agent_id` is `claude-code-<cwd-basename>` — a PROJECT DIRECTORY with ephemeral sessions, no process and no model id written down. Gate that with a **persona check at the point of production** (Critical Rule 26(c)), not a dollar threshold, and withhold only the offer. (d) `relearn`'s `edit_before_read` shipped a card whose own fix text says the harness already handles it and no rule is needed. `is_placeholder_fix` cannot see this — the text is a real, specific sentence that happens to say there is nothing to do — so the family carries an explicit `advisory_only` flag and `REASON_ADVISORY_ONLY`, distinct from the placeholder reason because "no template matched" and "none is needed" are different statements. In all four the observation stands and `past_overspend_*` is untouched (Critical Rule 32).
|
|
215
|
+
|
|
216
|
+
**Fix text lives in ONE catalog with a lint, because scattered constants are why the above could happen.** `core/fixes/` holds one `FixRecord` per fix type — text, delivery kind, personas, lever, the observation it answers, and the shapes it may not excuse — and every analyzer reads its text from there instead of defining a constant. The proof this is structural, not tidiness: the identical sizing contradiction shipped in BOTH the subagent rubric and the resend right-sizing template in different words, so correcting the reported one left the other live. Two copies of a policy is two policies. `core/fixes/lint.py` checks the properties mechanically — under the published line ceiling, long enough to be an instruction, no escape hatch the agent grades itself against, advisory-only text never offered as a write, fixed-point behaviour escalated to a hook — and above all: **a fix may not re-license the behaviour its own analyzer bills for**, which is the whole D-class and is now caught by a substring check against the record's own `must_not_relicense` set rather than by review. Two dimensions the catalog carries that a bare string cannot: **persona** (a Claude Code user edits instruction files, agent files and hooks; an SDK service edits routing code and redeploys — handing either the other's fix names an action they cannot take) and **lever**, where `model` and `effort` are separate and not substitutes: a worker that did not know enough needs a bigger model, one that did not try hard enough needs more effort.
|
|
217
|
+
|
|
218
|
+
**One instruction, one record — and the duplicate check must use CONTAINMENT, not Jaccard.** Three analyzers (`resend`, `downsize`, `relearn`'s `context_overflow`) each authored their own wording of "delegate context-heavy work to a subagent", so three near-identical blocks could land in one `CLAUDE.md`. The write budget's one-block-per-family rule cannot see this: they are three families from three analyzers. The harm is not untidiness — **length and redundancy reduce adherence**, so writing a rule three times makes it LESS likely to be followed than writing it once, and each analyzer's copy actively defeats the others. Their CLAIMS stay disjoint (Critical Rule 27 is untouched — they price different span populations); only the instruction is shared, with per-analyzer framing living in the card's advise text rather than in a second copy of the rule. The lint's metric is load-bearing and the obvious choice is wrong: **Jaccard divides by the union, so it collapses when two texts differ in LENGTH, which is exactly this defect's shape** — measured against the real wordings, the two duplicates scored 24% and 17% Jaccard against the rule they duplicated and would have been certified clean. Containment (`|a ∩ b| / min(|a|,|b|)`) scores the same pairs at 67% and 72%. A check that cannot catch what it was written for is worse than no check, because it reports the problem as absent.
|
|
219
|
+
|
|
220
|
+
**Two surfaces rendering one finding must lead with the SAME fix, through the same helper.** The Review inbox card led with the durable offload rule while `tj optimize`'s CLI branch led with `/compact`, for the identical finding — and the CLI led with the weaker one, which `COMPACTION_FIX`'s own text disclaims ("never fixes the pattern going forward; treat it as immediate relief, not the durable fix"). Both now render through `cost_proposals.compound_offload_fix`, so they cannot drift into different wordings again. Related: **`/compact` is never listed as an actionable lever.** It is transient, persists nothing past the session, and users who feel a session filling already do it unprompted — it sits below the actionable ceiling recorded in `.claude/product-state/ANALYZER-PERSONA-MATRIX.md`. Listing it beside durable levers implies it is the same kind of thing, and pads a list whose length is itself a cost to adherence. It survives only in an explicitly-labelled "immediate relief in an already-full session" position.
|
|
221
|
+
|
|
222
|
+
**A path-scoped rule is not a cheaper way to say the same thing — it changes WHICH fixes are offerable at all.** A `.claude/rules/*.md` carrying a `paths:` glob loads only when Claude reads a matching file, so only its frontmatter is resident (measured on a real record: ~13% of the same words in a `CLAUDE.md`). Because standing cost is an INPUT to the offer decision, that near-zero rent flips fixes from net-negative to net-positive: the same observation that cannot carry a `CLAUDE.md` rule can carry a scoped one. Three things are load-bearing. **(a) The frontmatter is the entire reason it is cheap** — a rule in the same directory WITHOUT `paths:` loads at launch with `CLAUDE.md` priority, so `load_semantics.classify` takes the file TEXT and decides on `has_paths_scope`, never on the directory; a path-scoped kind that emits no globs has silently become an always-resident rule while still being priced as if it were not, and the renderer refuses rather than degrading because the failure is invisible (the file is written, the rule works, only the cost is wrong). Accept both YAML forms — a block list's inline value is empty, so an inline-only check reads a perfectly good scoped rule as unscoped. **(b) Selection is DERIVED from the LEVER, never preferred.** A rule about the shape of the next action (model, effort, offload, routing) decides something *before any file is read*; scoped to globs it would load only after the decision it governs was made — present, well-formed and useless, which is worse than paying the rent and is invisible. Only file-shaped (awareness) fixes may scope, and only when the record names its own globs; inferring a glob from prose is guessing which files an instruction is about. **(c) Brace expansion has a budget** — past it a pattern is used UNEXPANDED and matches nothing, so the rule reads as applied while never firing; refuse such a glob rather than emitting a silent no-op.
|
|
223
|
+
|
|
224
|
+
**Hooks are already written; what was wrong was their PRICE.** `relearn_apply` has shipped hook generation for some time (guard + reactive renderers, the settings patch, the enforcement wiring), so "nothing writes a hook" is not the gap — three of the four rung-3 families that ship are `PostToolUseFailure` reactive hooks that inject `hookSpecificOutput.additionalContext`, and the rung ladder priced all of them at ZERO. For an executing guard that zero is earned; for an injecting hook it is false, and its cost is worse-behaved than a rule's, because the injected block lands in the conversation and is re-sent on every subsequent turn rather than being bounded by session count. Hence two delivery kinds, not one, each with its own pricer. Three rails the generated hook must carry, and they are not interchangeable with a rule's: **`additionalContext` MUST be nested inside `hookSpecificOutput`** (at top level it is silently ignored — the hook runs, exits 0, injects nothing, and looks like it worked); **the per-session cap lives in the script**, keyed on the stdin `session_id`, because only `SessionStart` is structurally once-per-session and the `once` field is honored solely in skill frontmatter, never in a settings file; and **injection is advisory, fail-open, never blocking by default** — a misfiring blocking hook degrades every session until removed, an asymmetry that justifies the rails on its own. Hook entries MERGE across settings layers rather than overriding, so a write cannot clobber the user's hooks but also must not assume it is the only writer.
|
|
225
|
+
|
|
226
|
+
**Ground a behavioural rule in the evidence it came from — by REPLACING vague words, never by appending them.** The behavioural fixes (`downsize`'s driver-role card, `resend`'s offload rule, `relearn`'s rung-1 notes) described a HABIT generically while the analyzer that produced them knew which directories the user worked in, which tools drove the tail and which models ran it. That is the shape of instruction the published guidance says gets ignored ("concrete enough to verify": `API handlers live in src/api/handlers/` beats `keep files organized`). Four rules. **(a) Substitution, never generation** — a template carrying "you did X in Y" beats both a generic sentence and a model call: no cost, no latency, no new failure mode, and it cannot hallucinate a directory the user does not have. **(b) SPECIFIC MUST NOT MEAN LONGER.** The guidance is specific *and concise*, and a longer instruction file reduces adherence — the very thing grounding is for — so a substitution REPLACES a vague span and a grounded rendering that came out materially longer is DISCARDED for the generic text (`grounding.MAX_GROUNDED_GROWTH`). On the reference corpus the grounded offload rule is 94% the length of the generic one it replaces, naming three real repos in place of a parenthesised enumeration; the failure mode to guard against is the generic sentence plus an evidence paragraph, which looks like an improvement and is a regression. **(c) Grounding lives in RENDERING, never in the record set** — one catalog record per fix, always; minting a record per user or per finding makes the catalog and its duplicate lint meaningless overnight, so the record declares which spans may be replaced and the renderer fills them. **(d) Degrade per-substitution, and never invent specificity** — an unobserved slot skips its own substitution and keeps that generic span, so a rule can be grounded in its directory even when the tool mix was not observed; an empty field means not observed and is never filled with a plausible-looking value. Mechanical fixes (`deadweight`'s "remove server X", `subagent`'s "set `model:` here") are already concrete by construction and are deliberately left alone — this is specific to rules that describe a habit rather than an edit.
|
|
227
|
+
|
|
228
|
+
**A user's DISMISSAL is a decision and belongs server-side — durable, reversible, and figure-safe.** It used to live in the browser (`localStorage`), which means it was not really recorded: a cleared profile, a second browser or a different machine and every dismissed card came back. It now lands in `dismissed.json` beside the DB, the same lock-free sink the applied/reverted ledgers use — and **not** a DuckDB table, for a structural reason rather than a historical one: `tj rules` is in `no_db_commands` and must answer while `tj serve` holds the write lock, and DuckDB permits one writer OR many readers with no concurrent read-only escape, so a dismissal stored in DuckDB could not be READ by the command that has to honour it whenever the daemon is up. Three properties, each a test. **(a) It suppresses the OFFER only** (Critical Rule 32) — the behaviour happened and cost what it cost, and "not this one" is a statement about our recommendation, not about the user's bill; the record is byte-identical with and without the dismissal except the offer fields. **(b) It must be REVERSIBLE, and durability is exactly what makes that non-negotiable** — a dismissal that cannot be undone trades a card that came back on every browser for one that never comes back anywhere, so the row stays listed carrying its state and `undismiss` restores it. **(c) It resolves through the ONE matcher** (`cost_apply.signature_is_applied`) and states its exclusion filter once, like its two sibling ledgers — a dismissal that did not honour the legacy-signature equivalence would let a card dismissed under one signature reappear under its refinement. Browser-local dismissals are not migrated: `localStorage` is unreadable server-side, so they simply lapse, and nothing in the UI pretends otherwise.
|
|
229
|
+
|
|
230
|
+
**A flag set UPSTREAM of a pass that rewrites the same field is not a flag, it is a suggestion.** `relearn`'s advisory families set `write_offered=False` at proposal construction, and `_apply_write_budget` then assigned `write_offered=decision.offered` from an allocator that knows nothing about advisory — so the withdrawal held in the unit test (which exercised the constructor) and was silently undone on the live daemon. **Unit tests cannot see this class**: they call the stage that sets the field, not the stage that overwrites it. The fix is to keep such a record out of the later pass entirely rather than to set the field twice. Two general lessons. (a) When a value is written at two stages, the LATER one wins and the earlier one is decoration — either remove the earlier write or exclude the record from the later stage. (b) **A defect that survives a green suite is usually a stage-boundary defect**, so verify through the composed path (here `_apply_write_budget` directly) rather than through the constructor, and boot the daemon against a real corpus before believing a behavioural fix shipped. The same live pass found placement's per-destination session counts computed and never serialized — the payload carried only paths, so every consumer reported `sessions: 0` for destinations the resolver had counted correctly, which is the one figure that justifies placing a rule at all.
|
|
231
|
+
|
|
232
|
+
**A gate the user cannot pass is not a gate, it is a dead end — and a ceiling over an AGGREGATE stops being meaningful once the thing it gates can be placed elsewhere.** The write budget's standing ceiling withheld every rule whenever the user's agent files were collectively too large, and told them to compress. Measured on seven real instruction files across six repos, compression returns a ~0.952 prose ratio (much of it whitespace reflow), so the two largest cards this product produces were gated behind a remedy that returns almost nothing. Two fixes, and both generalise. **Name a remedy that works**: these files are long by ACCUMULATION (numbered incident lists, append-only logs), where what frees space is pruning what no longer applies, expiring what was time-bound, and moving file-specific guidance into a path-scoped rule — compression works on prose, and these files are not long because their prose is verbose. **Scope the gate to what it is actually about**: the ceiling now asks whether THIS destination is too large, not whether the corpus is, because one pathological root file was blocking writes into nearly-empty project files with ample headroom. An aggregate gate was correct when there was one destination; placement made it answer a question nobody asked.
|
|
233
|
+
|
|
206
234
|
**Net-of-standing-cost is part of the contract, not an extra.** A fix that WRITES a permanent artifact into the user's agent files is not free: a rung-1 `CLAUDE.md` rule is re-sent on every future session, forever. Any analyzer whose fix is such a write MUST report `past_overspend_*` **net** of that standing cost and park the pre-net figure on a `gross_*` field; a proposal may never claim a saving larger than its net value. The arithmetic lives in exactly one place, `core/optimize/write_budget.py`, which also owns the write budget (ranked, capped, one block per family, no generic-placeholder rule) and reads the agent-file footprint off the `summarize` analyzer so the product can never recommend adding rules to a file the same report says to compress. Two rules that fall out of it and are easy to get wrong: **rung matters** (rung 2 charges only the always-loaded skill frontmatter, rung 3+ hooks/config charge ZERO because they are executed, never sent as prompt text), and **the budget is spent in PER-SESSION tokens** while the saving is netted against the WINDOW total — charging the window total against the budget would shrink it as a user's session count grows, which is backwards. The 30-day session pace both sides are expressed on is defined once in `core/optimize/projection.py`; never re-derive a projection factor locally.
|
|
207
235
|
|
|
208
236
|
**The same load semantics govern READING a file, not just writing one.** "Rung matters" is a statement about how Claude Code delivers a file, so it holds identically when an analyzer PRICES an existing file. `core/summarize/load_semantics.py` is the single source of truth both sides classify through: a `CLAUDE.md` / `.claude/rules/*.md` is resident in full on every call of every loading session, while a `.claude/skills/*/SKILL.md`, `.claude/commands/*.md` or `.claude/agents/*.md` surfaces only its **frontmatter** that way and delivers its **body** on invocation. So the window figure is two separately-observed terms — `always-resident reduction x sessions x reads per session` **+** `on-demand reduction x OBSERVED invocations` — never one term applied to the whole file. Getting this wrong in either direction is a real defect: pricing a whole skill body as always-resident made an uninvoked 160 KB skill library the single most expensive prompt file a user owns and pushed the cross-analyzer token rollup to 115% of the window's billed tokens; pricing an invoked body at zero would deny that a long skill that really runs costs anything. The invocation multiplier is OBSERVED from Claude Code transcripts (`core/summarize/invocations.py`: `Skill` tool calls, typed `<command-name>` slash commands, `Task`/`Agent` `subagent_type`), never assumed — and "invoked zero times" (a measurement, prices the body at zero) is a different statement from "no transcript corpus" (not a measurement; Critical Rule 28 corollary (a) degrades BOTH fields).
|
|
@@ -228,6 +256,7 @@ Post-ingest hooks run after each span is written to DB:
|
|
|
228
256
|
- **`tokenjam/api/deps.py`**: `require_api_key` — FastAPI dependency for optional API key auth on GET endpoints. Only enforced when `api.auth.enabled = true` in config.
|
|
229
257
|
- **`tokenjam/api/routes/`**: One file per resource — `spans.py` (OTLP JSON ingest), `traces.py`, `cost.py`, `cost_compare.py`, `tools.py`, `alerts.py`, `drift.py`, `optimize.py`, `reuse.py` (`GET /api/v1/reuse/clusters` — the Reuse finding plus skeleton-rendering extras `planning_texts` + `pricing_mode`; a dedicated endpoint (not bolted onto `/optimize`) so the per-cluster planner text, which can be many KB, isn't paid for on every Overview poll — #154), `budget.py`, `status.py`, `agents.py`, `metrics.py` (Prometheus text format from DB queries), `version.py` (unauthenticated `GET /health` → `{"status":"ok","version":...}` mounted with no prefix, plus `GET /api/v1/version`; the version is derived at runtime via `importlib.metadata.version("tokenjam")` — no hardcoded literal). **The dollar-bearing read routes (`/cost`, `/cost/compare`, `/optimize`, `/budget`) each return a `framing` block** (see `core/framing.py`) so the web UI renders plan-tier-aware figures without re-deriving the rules in JS. `/optimize` takes `?fast=true` to skip the expensive Trim analyzer (returns `skipped_analyzers`) for the polling Overview; `/cost` returns a window-bucketed `series` for the chart (see Web UI below). **Concurrency:** the sync (`def`) read routes (`/optimize`, `/cost/compare`) run in Starlette's threadpool, so concurrent requests reach the DB from multiple threads. `DuckDBBackend.conn` is a **per-thread DuckDB cursor** (`threading.local`) over one shared database — cursors are independent connections safe for concurrent use, so fan-out callers (the Overview) can fetch in parallel. Fixed in #124 (was a single shared connection that aborted under concurrent access); do not collapse `conn` back to one shared connection object.
|
|
230
258
|
- **`tokenjam/core/summarize/`**: Structure-aware prompt summarization (advisory; new feature). `detect.py` classifies prose vs. structure (fenced/inline code, tags, templates, tables); `candidates.py` (+ `catalog.py`/`estimate.py`) powers the `tj summarize list` scan; `wrap.py` is the pure protect→restore algorithm (wrap each structured span behind an id'd `<tj-keep>` marker, restore verbatim by id — structure is a hard guarantee); `session.py` is the no-scratch `prepare`/`check` lifecycle + staging (re-derives the wrap from the live file + a content hash; persists nothing but the staged result); `apply.py` writes a staged rewrite back to the file (default dry-run; `--go` writes) behind an owner + content-hash + symlink guard, with a gzip backup and `undo`; `backup.py` stores the gzipped original + metadata under `~/.tj/summary/backups/`; `delivery.py` is the CLI's automated rewrite step — `claude -p` (subprocess, timeout-guarded) or the Anthropic API (lazy `httpx` + the user's own `TJ_ANTHROPIC_API_KEY`) — plus the "pays for itself" amortization. Pure domain logic — no `tokenjam.cli`/`tokenjam.api` imports (delivery's API path lazily imports `httpx`, its lone outbound dependency).
|
|
259
|
+
- **`tokenjam/core/rulewrite/`**: The ONE rule-write lifecycle every rule-writing analyzer shares (`plan.py` list, `apply.py` stage/check/apply/undo, `store.py` staging + gzip backups, `types.py` shapes). Pure domain — no `tokenjam.cli`/`tokenjam.api` imports. Reuses `core/summarize/apply`'s guard model and `relearn_apply`'s block renderer rather than reimplementing either; the store persists rendered OUTPUT, never a recipe, so what a reviewer approved in the diff is byte-for-byte what apply writes. Surfaced by `tj rules` and `/api/v1/rules/*`. `core/optimize/rule_placement.py` is the WHERE half — named that, not `placement`, because `placement` is already a registered analyzer name for an unrelated question (Batch API lane, `analyzers/batch_placement.py`; Critical Rule 19).
|
|
231
260
|
- **`tokenjam/mcp/server.py`**: FastMCP stdio server exposing observability data to Claude Code (plus the summarize tools — `list_summarize_candidates`, `summarize_prep`, `summarize_check`, `summarize_apply`, `summarize_undo`; see `core/summarize/`). Uses either a read-only DuckDB connection or HTTP proxy to `tj serve`. Initialized via `init()` from `cmd_mcp.py`.
|
|
232
261
|
- **`tokenjam/cli/main.py`**: Root Click group with global options (`--config`, `--json`, `--no-color`, `--db`, `--agent`, `-v`). Registers all subcommands.
|
|
233
262
|
|
|
@@ -248,13 +277,15 @@ Post-ingest hooks run after each span is written to DB:
|
|
|
248
277
|
|
|
249
278
|
- **`tj summarize`** (`cmd_summarize.py`) — structure-aware prompt summarization (advisory; new feature). `list` scans for prompt files worth summarizing (catalog-default; any scope-widening flag opens it to `*.md`) and estimates the per-call token saving — read-only. `prep`/`check` are the mechanism: `prep` wraps a prompt's structure behind id'd `<tj-keep>` markers and emits it for rewriting (CLI manual/copy, or the MCP `summarize_prep`/`summarize_check` tools for in-session rewrites); `check` re-reads + hash-guards the file, restores every block verbatim by id, and stages the result **only if structure survives** (a hard gate). No scratch state — the file on disk is the source of truth; `summarize` is in `no_db_commands` (config-only). `apply` writes a staged rewrite back to the file — default **dry-run** (it prints the unified diff), `--go` writes — guarded by an owner + content-hash + symlink check and a gzip backup; `undo` restores from that backup (refusing on drift). `prep --via claude-p` (your local `claude -p`, no key) or `prep --via api` (Anthropic with your `TJ_ANTHROPIC_API_KEY` + the required `[summarize] api_model`, reporting a "pays for itself" amortization) runs the rewrite for you in one shot — both still pass the `check` gate before staging.
|
|
250
279
|
|
|
280
|
+
- **`tj rules`** (`cmd_rules.py`, `core/rulewrite/`) — the permanent CLAUDE.md rules the analyzers propose, and WHERE each one goes. `list`/`show` read the proposal cache the optimize pass already wrote (`rules` is in `no_db_commands` — config only, no analyzer sweep, works while `tj serve` holds the DuckDB lock); `stage` renders one diff PER DESTINATION; `check` re-verifies each against the file as it stands now; `apply` writes (default **dry-run**, `--go` writes, gzip backup first); `undo` reverts per destination; `applied` lists what can still be undone. The safety model is `core/summarize/apply`'s, reused rather than reimplemented (owner + content-hash + symlink guards, atomic write, refuse-on-drift undo), and the block rendering is `relearn_apply.render_note_content`'s, so a re-apply replaces its own marked block instead of duplicating it and the existing Revert path can still find it. **Partial outcomes are first-class:** three project files written and a fourth skipped because it drifted is the reported result, never rounded up to "applied". Mirrored at `GET/POST /api/v1/rules/*` and on the Lens **Optimize ▸ Rules** screen (`#/optimize/rules`), which exists because a Review-inbox card can express one write and cannot express "this rule, into these 4 of your 11 projects, here is each diff, apply selectively".
|
|
281
|
+
|
|
251
282
|
- **`tj pricing list`** (`cmd_pricing.py`) — read-only inspection of the resolved model pricing table. Prints one row per `(provider, model)` with `input` / `output` / `cache_read` / `cache_write` rates and a `source` column (`override` / `packaged`) showing which layer the rate resolved from. Supports `--json` and `--model <substring>` (case-insensitive filter). `pricing` is in `no_db_commands` (config-only). The source derivation lives in `core/pricing.load_pricing_sources()` (public) — the CLI reads it rather than re-deriving precedence; a listed row is always `packaged` or `override` (the `DEFAULT_INPUT_PER_MTOK` / `DEFAULT_OUTPUT_PER_MTOK` fallback in `core/pricing.py` applies only to models absent from the table, which never appear here). The `set` half of a future `tj pricing set|list` is not built yet (#282 shipped `list` only).
|
|
252
283
|
|
|
253
284
|
All commands support `--json` for machine-readable output. Commands that query alerts use exit code 1 if active (unacknowledged, unsuppressed) alerts exist.
|
|
254
285
|
|
|
255
286
|
**CLI testing pattern:** Tests use `click.testing.CliRunner` with `unittest.mock.patch` on `tokenjam.cli.main.load_config` and `tokenjam.cli.main.open_db` to inject an `InMemoryBackend` and test config. See `tests/integration/test_cli.py`. Note: `cmd_doctor` opens its own DuckDB connection via `config.storage.path` to verify writability — in tests you must set this to a real temp path (e.g. `tmp_path / "test.duckdb"`).
|
|
256
287
|
|
|
257
|
-
**`no_db_commands` in `cli/main.py`:** Commands that don't open the DB at startup —
|
|
288
|
+
**`no_db_commands` in `cli/main.py`:** Commands that don't open the DB at startup — read the set in `cli/main.py` rather than trusting an enumeration here. New commands that read only from config (or do their own DB connection later) should be added to this set so they work when `tj serve` holds the write lock. Tests for these commands can patch `open_db` with `side_effect=AssertionError(...)` to verify they never touch the DB.
|
|
258
289
|
|
|
259
290
|
**Test factories:** `tests/factories.py` provides `make_llm_span(billing_account="anthropic", ...)` and `make_session(plan_tier="api", ...)` with safe defaults that preserve existing test behavior. Tests exercising subscription / local / unknown plan-tier rendering paths should pass the field explicitly.
|
|
260
291
|
|
|
@@ -288,7 +319,7 @@ That routing lives behind **one seam, not per-command duck-typing**: `resolve_da
|
|
|
288
319
|
|
|
289
320
|
### Web UI ("TokenJam Lens")
|
|
290
321
|
|
|
291
|
-
`tokenjam/ui/index.html` is the served dashboard — a **single-file Preact + htm SPA** (no build step, no TypeScript, no client-side router). "TokenJam Lens" is the **brand only**: it appears in `<title>`, the sidebar wordmark, and the OpenAPI title, but never in module names, route paths, or config keys. Screens: **Overview** (the default landing route — a triage front door), Status, Traces, Cost, Alerts, Drift, Optimize, Budget.
|
|
322
|
+
`tokenjam/ui/index.html` is the served dashboard — a **single-file Preact + htm SPA** (no build step, no TypeScript, no client-side router). "TokenJam Lens" is the **brand only**: it appears in `<title>`, the sidebar wordmark, and the OpenAPI title, but never in module names, route paths, or config keys. Screens: **Overview** (the default landing route — a triage front door), Status, Traces, Cost, Alerts, Drift, Optimize (with **Summarize** and **Rules** sub-views), Budget.
|
|
292
323
|
|
|
293
324
|
- **Offline-first (Critical Rule 18):** every JS/CSS dep is vendored under `tokenjam/ui/vendor/` — Preact + hooks + htm (ESM via `<script type="importmap">`) and **uPlot** (vendored IIFE global `uPlot` + CSS, pinned in `docs/internal/lens-vendor-versions.md`). No render-time external HTTP. `tests/unit/test_ui_offline.py` enforces this; clickable `<a href>` links are the only allowed external URLs.
|
|
294
325
|
- **Single compute path:** the UI reads everything from the REST API and **never re-implements analysis, aggregation, or plan-tier framing in JS** — it consumes the `framing` block (see `core/framing.py`). If the UI needs a number, extend the endpoint; don't compute it client-side. This applies to gating/filter sets too, not just numbers: if a Python-side map decides which analyzers/findings apply to a persona (e.g. `core/optimize/runner.py`'s `PERSONA_DISABLED_ANALYZERS`), the API already publishes the resolved set (`persona_disabled_analyzers` on `/optimize`) — read that field everywhere the gate is needed, never re-declare the map as a second JS literal. Two call sites deriving the same set from two different sources will silently desync the moment either side edits its copy.
|
|
@@ -356,6 +387,10 @@ When a span has a `conversation_id` matching an existing session, it's attribute
|
|
|
356
387
|
|
|
357
388
|
38. **Do not assume a telemetry source carries an id for the thing it describes — check, then derive the key from stored COLUMNS when it does not.** Cross-observer accounting needs a name for the underlying API call, and the obvious design is to stamp the provider's own response id on both sides. That works wherever the observer sees the provider's response — `sdk/integrations/anthropic.py` stamps `gen_ai.response.id`, `core/backfill.py` stamps `tj.call_id` off the assistant message key. It does not work for the pairing that matters most on this product's dominant corpus: **Claude Code's own OTel log exporter emits no per-call identifier at all.** Read `ClaudeCodeEvents` in `otel/semconv.py` for what its `api_request` event actually carries, and confirm there before designing around an id — the absence is the kind of premise that silently invalidates a whole approach. A stamp only ONE side of a pair can carry is worse than no stamp: it yields two different keys for one call, so the pairing never fires and nothing looks broken. **Derive the cross-observer key from columns the store already holds rather than from an attribute an ingest path promises to write** — that also makes it readable on rows written before any stamping existed, which is what lets `tj doctor --repair` clean a database an older build already doubled. Keep stamping the ids that DO exist: they are exact where a derived key is only strong, and they are what a future exporter revision would let you key on directly. If you are about to rely on the absence, re-verify the exporter's attribute set rather than trusting this line.
|
|
358
389
|
|
|
390
|
+
39. **A flag set UPSTREAM of a pass that rewrites the same field is not a flag, it is a suggestion — and a unit test on the constructor cannot see it.** An analyzer correctly set `advisory_only` on a family and cleared its write offer at construction; a later pass, `_apply_write_budget`, then assigned `write_offered` from an allocator that knew nothing about the flag and quietly restored the offer. Every value on the field was individually correct at the moment it was written, and the last writer won. The unit test passed the whole time because it exercised the CONSTRUCTOR path directly while the daemon ran the COMPOSED path — the test was structurally incapable of observing the defect, so a green suite was evidence of nothing. **The fix shape is not a second guard in the later pass** (that is a race between two writers, which the next pass re-opens); it is to **remove the item from the INPUT the later pass consumes**, so no decision exists to overwrite. Here the advisory family never becomes a `WriteCandidate` at all. Two rules follow. *(a) When you set a field that a downstream pass also writes, either withdraw the row from that pass's input or move the decision into it — never both write it.* Grep for other assignments to the field before assuming your assignment is the final one. *(b) A test that constructs the object under test proves the constructor works and says nothing about the pipeline.* Assert through the composed path a user actually reaches (build the report, run the budget pass, then read the field), which is the same lesson Critical Rule 36 draws for repair passes.
|
|
391
|
+
|
|
392
|
+
40. **A fixture that is a real domain object can stop being a valid vehicle for what it tests while every assertion still passes — for the wrong reason.** Eight fixtures seeded one particular relearn failure family in order to exercise write-budget behaviour. That family later became advisory-only, so it can no longer demonstrate ANY budget behaviour at all: it is never offered, never priced, never ranked. The tests stayed green, because the assertions they made happened to hold vacuously on a family that had left the population under test. This happened twice on the same branch, with two different families. The hazard is specific to fixtures that are real domain objects rather than stubs: **changing a domain object's semantics silently invalidates every test that used it as scaffolding, and the suite reports success either way.** Three habits. *(a) When you change what a family, analyzer or kind MEANS, grep the tests for its name and re-read each use — asking not "does this still pass" but "can this fixture still exhibit the behaviour the test claims to check".* *(b) Prefer a fixture that would FAIL loudly if it stopped qualifying* — assert the precondition explicitly (`assert cluster.write_offered`) before asserting the behaviour, so the vehicle's own validity is pinned rather than assumed. *(c) When you do have to swap the fixture, say in the docstring which object it replaced and why the old one stopped qualifying* — that note is what stops the next reader from "restoring" it.
|
|
393
|
+
|
|
359
394
|
## Config
|
|
360
395
|
|
|
361
396
|
Config is TOML, discovered at: `tj.toml` -> `.tj/config.toml` -> `~/.config/tj/config.toml`. Override with `--config` or `TJ_CONFIG` env var. Full config hierarchy is in `tokenjam/core/config.py` (`TjConfig` dataclass).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tokenjam
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: TokenJam: a local-first, OTel-native cost-saving utility for AI agents
|
|
5
5
|
Project-URL: Homepage, https://tokenjam.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/Metabuilder-Labs/tokenjam
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
The fastest way onto TokenJam is to install it and run the onboarding wizard for your agent; that
|
|
4
4
|
wires up everything (statusline, backfill, dashboard) in one step. This page walks every entry path
|
|
5
|
-
by persona, most-common first; each
|
|
5
|
+
by persona, most-common first; each step ends with a way to verify it actually worked before you move
|
|
6
6
|
on.
|
|
7
7
|
|
|
8
8
|
> **Just looking first?** `npx tokenjam` prints a 15-second read-only report over the Claude Code logs
|
|
9
9
|
> you already have on disk (`~/.claude/projects/*.jsonl`); nothing installed, nothing written to disk,
|
|
10
|
-
> no daemon. You should see
|
|
11
|
-
> Code logs found," which just means you haven't run Claude Code on this machine yet). See
|
|
10
|
+
> no daemon. You should see one line telling you how much of your recent sessions was avoidable (or
|
|
11
|
+
> "No Claude Code logs found," which just means you haven't run Claude Code on this machine yet). See
|
|
12
12
|
> [docs/installation.md](installation.md) for the `uv`/`pipx` runner requirements. When you're ready to
|
|
13
|
-
> keep it, start at
|
|
13
|
+
> keep it, start at step 1 below.
|
|
14
14
|
|
|
15
15
|
## 1. Claude Code / Codex onboarding wizards
|
|
16
16
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenjam",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Zero-install launcher for TokenJam (tj): npx tokenjam runs the Python CLI via uvx/pipx and reports the recurring mistakes your AI agent keeps repeating, no setup required.",
|
|
5
5
|
"keywords": ["claude-code", "ccusage", "tokens", "cost-optimization", "llm", "agents", "observability", "tokenjam", "anthropic", "claude", "token-usage", "agent-behavior", "cli", "statusline", "opentelemetry", "agent-observability"],
|
|
6
6
|
"homepage": "https://tokenjam.dev",
|
|
@@ -12,7 +12,7 @@ from click.testing import CliRunner
|
|
|
12
12
|
|
|
13
13
|
from tokenjam.cli.main import cli
|
|
14
14
|
from tokenjam.core.config import AgentConfig, BudgetConfig, TjConfig
|
|
15
|
-
from tokenjam.core.db import InMemoryBackend
|
|
15
|
+
from tokenjam.core.db import SPANS_INDEX_SQL, InMemoryBackend
|
|
16
16
|
from tokenjam.core.models import (
|
|
17
17
|
AgentRecord,
|
|
18
18
|
Alert,
|
|
@@ -473,6 +473,12 @@ def _duckdb_with_dropped_migration_7(tmp_path):
|
|
|
473
473
|
backend.conn.execute(f"DROP INDEX IF EXISTS {idx}")
|
|
474
474
|
backend.conn.execute("ALTER TABLE spans DROP COLUMN request_params")
|
|
475
475
|
backend.conn.execute("ALTER TABLE spans DROP COLUMN request_tools")
|
|
476
|
+
# DuckDB refuses to drop a column while the table carries ART indexes, so
|
|
477
|
+
# the indexes above come off first — and have to go straight back on. Left
|
|
478
|
+
# off, this fixture would additionally reproduce the SEPARATE fault doctor's
|
|
479
|
+
# index-integrity check exists for, and these tests would be asserting an
|
|
480
|
+
# exit code that has nothing to do with the missing column they are about.
|
|
481
|
+
backend.conn.execute(SPANS_INDEX_SQL)
|
|
476
482
|
return backend
|
|
477
483
|
|
|
478
484
|
|
|
@@ -11,6 +11,8 @@ from datetime import timedelta
|
|
|
11
11
|
import httpx
|
|
12
12
|
import pytest
|
|
13
13
|
|
|
14
|
+
from tokenjam.core.rulewrite.kinds import DELIVERY_SKILL
|
|
15
|
+
|
|
14
16
|
from tokenjam.api.app import create_app
|
|
15
17
|
from tokenjam.core.config import ApiAuthConfig, ApiConfig, StorageConfig, TjConfig
|
|
16
18
|
from tokenjam.core.db import InMemoryBackend
|
|
@@ -198,7 +200,7 @@ async def test_apply_workspace_refuses_a_caller_supplied_proposed_fix(
|
|
|
198
200
|
|
|
199
201
|
|
|
200
202
|
async def test_cost_apply_workspace_writes_note_and_records_marker(app, client, db, monkeypatch, tmp_path):
|
|
201
|
-
"""A CC-origin subagent proposal routes a reversible
|
|
203
|
+
"""A CC-origin subagent proposal routes a reversible CLAUDE.md rule through the
|
|
202
204
|
existing relearn apply path, then records the cost marker for delta-verify."""
|
|
203
205
|
from tokenjam.core.optimize import relearn_apply as pa
|
|
204
206
|
|
|
@@ -253,17 +255,17 @@ async def test_cost_apply_workspace_writes_note_and_records_marker(app, client,
|
|
|
253
255
|
assert any(r["analyzer"] == "subagent" for r in applied["applied"])
|
|
254
256
|
|
|
255
257
|
|
|
256
|
-
async def
|
|
258
|
+
async def test_cost_apply_workspace_writes_skill_for_a_skill_proposal_and_reverts(
|
|
257
259
|
app, client, config, db, monkeypatch, tmp_path,
|
|
258
260
|
):
|
|
259
261
|
"""`apply-workspace` is generic across analyzers, not special-cased to
|
|
260
|
-
`subagent`: a
|
|
262
|
+
`subagent`: a skill-note proposal routes through the SAME path,
|
|
261
263
|
writes, and reverts cleanly.
|
|
262
264
|
|
|
263
265
|
Two facts are pinned here. First, a deterministic tool-call cluster on a
|
|
264
266
|
claude-code window produces NO `script` card any more: the analyzer has no
|
|
265
267
|
fix that survives for that persona and is skipped before it runs. Second,
|
|
266
|
-
the generic
|
|
268
|
+
the generic skill write/revert route still works, exercised by seeding the
|
|
267
269
|
proposal the adapter builds directly into the store."""
|
|
268
270
|
from tokenjam.core.optimize import relearn_apply as pa, relearn_store
|
|
269
271
|
from tokenjam.core.optimize.analyzers.workflow_restructure import (
|
|
@@ -322,7 +324,7 @@ async def test_cost_apply_workspace_writes_skill_for_a_rung2_proposal_and_revert
|
|
|
322
324
|
prop = script_props[0]
|
|
323
325
|
assert prop["apply_capable"] is True
|
|
324
326
|
assert prop["advise_only"] is False
|
|
325
|
-
assert prop["
|
|
327
|
+
assert prop["delivery"] == DELIVERY_SKILL
|
|
326
328
|
|
|
327
329
|
body = {"proposal_id": prop["proposal_id"], "target_path": str(target)}
|
|
328
330
|
|
|
@@ -212,11 +212,76 @@ def test_delete_spans_before_cutoff(db):
|
|
|
212
212
|
db.insert_span(span_new)
|
|
213
213
|
|
|
214
214
|
cutoff = now - timedelta(days=90)
|
|
215
|
-
|
|
216
|
-
assert
|
|
215
|
+
spans_deleted, sessions_deleted = db.delete_spans_before(cutoff)
|
|
216
|
+
assert spans_deleted == 1
|
|
217
217
|
|
|
218
218
|
remaining = db.conn.execute("SELECT COUNT(*) FROM spans").fetchone()
|
|
219
219
|
assert remaining[0] == 1
|
|
220
|
+
# The session straddles the cutoff and still has a live span, so it stays:
|
|
221
|
+
# a session is orphaned by the delete only once it has NO spans left.
|
|
222
|
+
assert sessions_deleted == 0
|
|
223
|
+
assert db.conn.execute("SELECT COUNT(*) FROM sessions").fetchone()[0] == 1
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_delete_spans_before_cutoff_takes_the_sessions_it_orphans(db):
|
|
227
|
+
"""Deleting only spans left the parent sessions asserting a day had data.
|
|
228
|
+
|
|
229
|
+
`core/data_span` unions `sessions.started_at` into the day set it measures
|
|
230
|
+
the available span from, so every orphan went on claiming a day carried data
|
|
231
|
+
after the data for that day was destroyed — the deletion skewed the measure
|
|
232
|
+
of what survived it.
|
|
233
|
+
"""
|
|
234
|
+
_insert_agent(db)
|
|
235
|
+
now = utcnow()
|
|
236
|
+
old = now - timedelta(days=100)
|
|
237
|
+
|
|
238
|
+
aged_out = make_session(started_at=old, ended_at=old + timedelta(minutes=1))
|
|
239
|
+
db.upsert_session(aged_out)
|
|
240
|
+
db.insert_span(make_llm_span(session_id=aged_out.session_id, start_time=old))
|
|
241
|
+
|
|
242
|
+
live = make_session()
|
|
243
|
+
db.upsert_session(live)
|
|
244
|
+
db.insert_span(make_llm_span(
|
|
245
|
+
session_id=live.session_id, start_time=now - timedelta(days=1),
|
|
246
|
+
))
|
|
247
|
+
|
|
248
|
+
spans_deleted, sessions_deleted = db.delete_spans_before(now - timedelta(days=90))
|
|
249
|
+
assert (spans_deleted, sessions_deleted) == (1, 1)
|
|
250
|
+
|
|
251
|
+
surviving = [
|
|
252
|
+
r[0] for r in db.conn.execute("SELECT session_id FROM sessions").fetchall()
|
|
253
|
+
]
|
|
254
|
+
assert surviving == [live.session_id]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_delete_spans_before_cutoff_takes_an_aged_out_session_with_no_spans(db):
|
|
258
|
+
"""A pre-cutoff session with no spans is aged-out history like any other.
|
|
259
|
+
|
|
260
|
+
Left behind it would go on asserting, to `core/data_span`, that a day
|
|
261
|
+
beyond the retention horizon carried data.
|
|
262
|
+
"""
|
|
263
|
+
_insert_agent(db)
|
|
264
|
+
now = utcnow()
|
|
265
|
+
empty = make_session(
|
|
266
|
+
started_at=now - timedelta(days=100),
|
|
267
|
+
ended_at=now - timedelta(days=100) + timedelta(minutes=1),
|
|
268
|
+
)
|
|
269
|
+
db.upsert_session(empty)
|
|
270
|
+
|
|
271
|
+
spans_deleted, sessions_deleted = db.delete_spans_before(now - timedelta(days=90))
|
|
272
|
+
assert (spans_deleted, sessions_deleted) == (0, 1)
|
|
273
|
+
assert db.conn.execute("SELECT COUNT(*) FROM sessions").fetchone()[0] == 0
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_delete_spans_before_cutoff_keeps_a_live_session_with_no_spans_yet(db):
|
|
277
|
+
"""An open session that has not written a span is not aged out."""
|
|
278
|
+
_insert_agent(db)
|
|
279
|
+
now = utcnow()
|
|
280
|
+
db.upsert_session(make_session(started_at=now, ended_at=None))
|
|
281
|
+
|
|
282
|
+
spans_deleted, sessions_deleted = db.delete_spans_before(now - timedelta(days=90))
|
|
283
|
+
assert (spans_deleted, sessions_deleted) == (0, 0)
|
|
284
|
+
assert db.conn.execute("SELECT COUNT(*) FROM sessions").fetchone()[0] == 1
|
|
220
285
|
|
|
221
286
|
|
|
222
287
|
# -- Traces --
|
|
@@ -18,6 +18,8 @@ from datetime import datetime, timedelta, timezone
|
|
|
18
18
|
import httpx
|
|
19
19
|
import pytest
|
|
20
20
|
|
|
21
|
+
from tokenjam.core.rulewrite.kinds import DELIVERY_CLAUDE_MD_RULE
|
|
22
|
+
|
|
21
23
|
from tokenjam.api.app import create_app
|
|
22
24
|
from tokenjam.core.config import ApiAuthConfig, ApiConfig, StorageConfig, TjConfig
|
|
23
25
|
from tokenjam.core.db import InMemoryBackend
|
|
@@ -67,6 +69,18 @@ def app(config, db):
|
|
|
67
69
|
cache_tokens=400, session_id=f"s-{i}",
|
|
68
70
|
start_time=now - timedelta(days=2, minutes=i),
|
|
69
71
|
))
|
|
72
|
+
# The corpus has to be 30 days WIDE, not merely 30 days' worth of rows. The
|
|
73
|
+
# inbox window is the analysis span bounded by how far back this store's
|
|
74
|
+
# oldest row actually sits, so a two-day corpus resolves to a two-day
|
|
75
|
+
# window — and the hand-built relearn buckets below, which are the point of
|
|
76
|
+
# these tests, are labelled `30d`. One row at the far edge makes the two
|
|
77
|
+
# sides name the same window without changing what any analyzer finds.
|
|
78
|
+
db.insert_span(make_llm_span(
|
|
79
|
+
agent_id="svc-a", provider="anthropic", model="claude-sonnet-5",
|
|
80
|
+
billing_account="anthropic", input_tokens=15_000, output_tokens=200,
|
|
81
|
+
cache_tokens=400, session_id="s-oldest",
|
|
82
|
+
start_time=now - timedelta(days=29),
|
|
83
|
+
))
|
|
70
84
|
pipeline = IngestPipeline(db=db, config=config)
|
|
71
85
|
return create_app(config=config, db=db, ingest_pipeline=pipeline)
|
|
72
86
|
|
|
@@ -103,7 +117,7 @@ def _cluster(
|
|
|
103
117
|
) -> RelearnCluster:
|
|
104
118
|
return RelearnCluster(
|
|
105
119
|
signature=signature, family_key=signature, title=f"{signature} recurs",
|
|
106
|
-
sessions=3, occurrences=9, repos=["demo"],
|
|
120
|
+
sessions=3, occurrences=9, repos=["demo"], delivery=DELIVERY_CLAUDE_MD_RULE, scope="project",
|
|
107
121
|
proposed_fix="Do the thing that stops this.",
|
|
108
122
|
past_overspend_usd=unbounded_usd, past_overspend_tokens=unbounded_tokens,
|
|
109
123
|
past_reread_usd=round(unbounded_usd * 0.1, 6),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
1. Mutating endpoints (apply/enable/disable/revert/refresh) require the
|
|
5
5
|
always-on local write token, independent of ``api.auth.enabled``.
|
|
6
6
|
2. ``/apply`` refuses a ``target_path`` outside the user's home directory
|
|
7
|
-
(defense-in-depth allowlist) and — for
|
|
7
|
+
(defense-in-depth allowlist) and — for a CLAUDE.md rule — a target that isn't an
|
|
8
8
|
allowlisted note file (see test_relearn_apply.py for the relearn_apply
|
|
9
9
|
unit-level version of that same guard).
|
|
10
10
|
|
|
@@ -16,6 +16,8 @@ from __future__ import annotations
|
|
|
16
16
|
import httpx
|
|
17
17
|
import pytest
|
|
18
18
|
|
|
19
|
+
from tokenjam.core.rulewrite.kinds import DELIVERY_CLAUDE_MD_RULE, DELIVERY_INJECTING_HOOK
|
|
20
|
+
|
|
19
21
|
from tokenjam.api.app import create_app
|
|
20
22
|
from tokenjam.core.config import ApiAuthConfig, ApiConfig, StorageConfig, TjConfig
|
|
21
23
|
from tokenjam.core.db import InMemoryBackend
|
|
@@ -72,7 +74,7 @@ def stored_proposal(config) -> str:
|
|
|
72
74
|
cluster = RelearnCluster(
|
|
73
75
|
signature="cwd_confusion", family_key="cwd_confusion",
|
|
74
76
|
title="cwd / relative-path confusion", sessions=5, occurrences=9,
|
|
75
|
-
repos=["demo"],
|
|
77
|
+
repos=["demo"], delivery=DELIVERY_CLAUDE_MD_RULE, scope="project",
|
|
76
78
|
proposed_fix="Verify an absolute cwd before a relative Read.",
|
|
77
79
|
)
|
|
78
80
|
relearn_store.write_cache(RelearnFinding(clusters=[cluster]), config=config)
|
|
@@ -212,7 +214,7 @@ async def test_apply_allows_target_inside_home(app, client, monkeypatch, tmp_pat
|
|
|
212
214
|
assert r.json()["dry_run"] is False
|
|
213
215
|
|
|
214
216
|
|
|
215
|
-
# --- must-fix #2 (routed through the API):
|
|
217
|
+
# --- must-fix #2 (routed through the API): note target allowlist -------------
|
|
216
218
|
|
|
217
219
|
async def test_apply_note_route_refuses_non_markdown_target(
|
|
218
220
|
app, client, monkeypatch, tmp_path, stored_proposal,
|
|
@@ -257,7 +259,7 @@ def _store_cost_proposal(config, **overrides) -> str:
|
|
|
257
259
|
"target_key": {}, "evidence": "", "baseline": {},
|
|
258
260
|
"advise_text": "Route explore to the cheaper same-family model.",
|
|
259
261
|
"proposed_fix": "Route explore to the cheaper same-family model.",
|
|
260
|
-
"
|
|
262
|
+
"delivery": "", "scope": "project", "apply_capable": True,
|
|
261
263
|
"apply_kind": "agent_model", "agent_name": "explore",
|
|
262
264
|
"current_model": "claude-opus-4-8", "proposed_model": "claude-haiku-4-5",
|
|
263
265
|
}
|
|
@@ -405,7 +407,7 @@ async def test_apply_refuses_a_stored_proposal_missing_a_required_field(
|
|
|
405
407
|
assert target.read_text() == _AGENT_FILE
|
|
406
408
|
|
|
407
409
|
|
|
408
|
-
async def
|
|
410
|
+
async def test_delivery_apply_opens_no_cost_window(
|
|
409
411
|
app, client, config, monkeypatch, tmp_path, stored_proposal,
|
|
410
412
|
):
|
|
411
413
|
"""A plain note fix has no priced metric, so it must not create a cost
|
|
@@ -463,7 +465,8 @@ async def test_apply_rejects_a_client_constructed_cluster_payload(
|
|
|
463
465
|
target.write_text("# Repo\n", encoding="utf-8")
|
|
464
466
|
|
|
465
467
|
body = _apply_body(str(target), proposal_id=stored_proposal)
|
|
466
|
-
body.update({"signature": "attacker", "
|
|
468
|
+
body.update({"signature": "attacker", "delivery": DELIVERY_INJECTING_HOOK,
|
|
469
|
+
"title": "not from the detector",
|
|
467
470
|
"proposed_fix": "rm -rf /"})
|
|
468
471
|
r = await client.post(
|
|
469
472
|
"/api/v1/relearn/apply", json=body,
|
|
@@ -607,7 +610,7 @@ async def test_proposals_flag_example_sessions_that_resolve(config, db, client):
|
|
|
607
610
|
cluster = RelearnCluster(
|
|
608
611
|
signature="cwd_confusion", family_key="cwd_confusion",
|
|
609
612
|
title="cwd / relative-path confusion", sessions=2, occurrences=3,
|
|
610
|
-
repos=["demo"],
|
|
613
|
+
repos=["demo"], delivery=DELIVERY_CLAUDE_MD_RULE, scope="project",
|
|
611
614
|
proposed_fix="Verify an absolute cwd before a relative Read.",
|
|
612
615
|
examples=[
|
|
613
616
|
RelearnExample(session_id="ingested-1", repo="demo", ts=None, snippet="a"),
|
|
@@ -641,7 +644,7 @@ async def test_advise_only_proposals_carry_their_reason_in_the_payload(config, c
|
|
|
641
644
|
advise = RelearnCluster(
|
|
642
645
|
signature="http_call:peer closed", family_key=None,
|
|
643
646
|
title="peer closed the connection", sessions=3, occurrences=9,
|
|
644
|
-
repos=["billing-svc"],
|
|
647
|
+
repos=["billing-svc"], delivery=DELIVERY_CLAUDE_MD_RULE, scope="project",
|
|
645
648
|
proposed_fix="Retry the upstream call with backoff.",
|
|
646
649
|
examples=[RelearnExample(session_id="s1", repo="billing-svc", ts=None, snippet="e")],
|
|
647
650
|
advise_only=True,
|
|
@@ -649,7 +652,7 @@ async def test_advise_only_proposals_carry_their_reason_in_the_payload(config, c
|
|
|
649
652
|
workspace = RelearnCluster(
|
|
650
653
|
signature="cwd_confusion", family_key="cwd_confusion",
|
|
651
654
|
title="cwd / relative-path confusion", sessions=4, occurrences=12,
|
|
652
|
-
repos=["demo"],
|
|
655
|
+
repos=["demo"], delivery=DELIVERY_CLAUDE_MD_RULE, scope="project",
|
|
653
656
|
proposed_fix="Verify an absolute cwd before a relative Read.",
|
|
654
657
|
)
|
|
655
658
|
relearn_store.write_cache(RelearnFinding(clusters=[advise, workspace]), config=config)
|
|
@@ -35,6 +35,26 @@ from tests.factories import make_session
|
|
|
35
35
|
ANCHOR = datetime(2026, 7, 27, 12, 0, tzinfo=timezone.utc)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def utc_noon(days_ago: int) -> datetime:
|
|
39
|
+
"""A seed instant that cannot straddle a day boundary, in any timezone.
|
|
40
|
+
|
|
41
|
+
The day-span measure counts DISTINCT UTC days and discards anything dated
|
|
42
|
+
later than today, so a fixture seeded at an offset from ``utcnow()`` puts
|
|
43
|
+
its rows wherever the wall clock happens to be: rows land on one calendar
|
|
44
|
+
day for part of the day and two for the rest, and a row seeded at "now" can
|
|
45
|
+
read as tomorrow under a database timezone that runs ahead of UTC. Both
|
|
46
|
+
made the assertions below go red for a few hours out of every day and green
|
|
47
|
+
again afterwards, which reads as "whoever pushed last broke it".
|
|
48
|
+
|
|
49
|
+
Noon UTC is the furthest any instant can be from both boundaries, so the
|
|
50
|
+
day a row belongs to is fixed no matter when or where the suite runs.
|
|
51
|
+
"""
|
|
52
|
+
today = datetime.now(tz=timezone.utc).date()
|
|
53
|
+
return datetime(
|
|
54
|
+
today.year, today.month, today.day, 12, 0, tzinfo=timezone.utc,
|
|
55
|
+
) - timedelta(days=days_ago)
|
|
56
|
+
|
|
57
|
+
|
|
38
58
|
@pytest.fixture
|
|
39
59
|
def db():
|
|
40
60
|
backend = InMemoryBackend()
|
|
@@ -203,7 +223,9 @@ async def test_a_cache_without_windowed_figures_says_so_rather_than_faking_one(
|
|
|
203
223
|
@pytest.mark.asyncio
|
|
204
224
|
async def test_the_days_of_data_available_measure_is_served(client, config, db):
|
|
205
225
|
_seed_relearn_cache(config)
|
|
206
|
-
db.upsert_session(make_session(
|
|
226
|
+
db.upsert_session(make_session(
|
|
227
|
+
session_id="s1", agent_id="claude-code-x", started_at=utc_noon(0),
|
|
228
|
+
))
|
|
207
229
|
async with client as c:
|
|
208
230
|
body = (await c.get("/api/v1/relearn/proposals")).json()
|
|
209
231
|
span = body["data_span"]
|
|
@@ -220,8 +242,9 @@ async def test_an_ancient_row_does_not_inflate_the_served_span(client, config, d
|
|
|
220
242
|
actually carries data on.
|
|
221
243
|
"""
|
|
222
244
|
_seed_relearn_cache(config)
|
|
223
|
-
|
|
224
|
-
|
|
245
|
+
db.upsert_session(make_session(
|
|
246
|
+
session_id="s1", agent_id="a", started_at=utc_noon(0),
|
|
247
|
+
))
|
|
225
248
|
db.upsert_session(make_session(
|
|
226
249
|
session_id="ancient", agent_id="a",
|
|
227
250
|
started_at=datetime(2020, 1, 1, tzinfo=timezone.utc),
|