claude-code-conductor 2.20.0__tar.gz → 2.22.0__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.
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/select_tier.py +9 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/session_stop.py +17 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/scripts/record_tier_outcome.py +5 -1
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/CHANGELOG.md +60 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/PKG-INFO +1 -1
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/__init__.py +1 -1
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_tier.py +44 -1
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/db.py +352 -3
- claude_code_conductor-2.22.0/src/c3/migrations/002_agent_cost_runs.sql +41 -0
- claude_code_conductor-2.22.0/src/c3/migrations/003_tier_cost.sql +25 -0
- claude_code_conductor-2.22.0/src/c3/pricing.py +157 -0
- claude_code_conductor-2.22.0/src/c3/usage_ingester.py +388 -0
- claude_code_conductor-2.22.0/tests/fixtures/usage/README.md +18 -0
- claude_code_conductor-2.22.0/tests/fixtures/usage/mainline.jsonl +5 -0
- claude_code_conductor-2.22.0/tests/fixtures/usage/subagents/agent-deadbeef.jsonl +3 -0
- claude_code_conductor-2.22.0/tests/fixtures/usage/subagents/agent-deadbeef.meta.json +1 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_record_tier_outcome.py +115 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_select_tier.py +82 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_session_start.py +11 -5
- claude_code_conductor-2.22.0/tests/test_cli_tier.py +507 -0
- claude_code_conductor-2.22.0/tests/test_db.py +518 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_migrate.py +215 -0
- claude_code_conductor-2.22.0/tests/test_pricing.py +225 -0
- claude_code_conductor-2.22.0/tests/test_usage_ingester.py +529 -0
- claude_code_conductor-2.20.0/tests/test_cli_tier.py +0 -258
- claude_code_conductor-2.20.0/tests/test_db.py +0 -166
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/CLAUDE.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/architect.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/code-reviewer.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/developer.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/doc-writer.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/interviewer.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/planner.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/project-setup.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/security-reviewer.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/systematic-debugger.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/tester.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/wt_developer.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/wt_systematic-debugger.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/agents/wt_tester.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/breaking-changes.txt +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/deletions.txt +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/docs/config-policy.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/docs/parallel-agents-setup.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/docs/platform-adapters.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/docs/settings.json.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/_hook_utils.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/check_agent_invocation.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/consolidate_memory.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/permission_handler.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/permission_handler_toast.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/planner_check.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/post_tool.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/pre_compact.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/pre_tool.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/recall_inject.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/restore_session.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/session_start.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/session_utils.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/statusline.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/stop.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/hooks/worktree_guard.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/memory/.gitkeep +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/permission_rules.json +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/rules/promoted/index.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/settings.json +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/codex-review/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/references/code-review-checklist.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/references/plan-design-guidelines.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/references/security-review-checklist.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/scripts/record_review_decision.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/dev-workflow/scripts/review_hint_inject.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/develop/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/doc/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/extract-lib/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/init-session/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/mcp-config/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/parallel-agents/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/pattern-status/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/promote-pattern/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/recall/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/report-timestamp/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/report-timestamp/scripts/get_timestamp.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/review-phase/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/setup/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/setup/reference.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/setup/templates/coding-standards-template.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/setup/templates/project-conventions-template.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/skills/start/SKILL.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.claude/state/.gitkeep +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/.gitignore +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSE +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSES/chroma-hnswlib-LICENSE +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSES/chroma-hnswlib-NOTICE +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSES/fastembed-LICENSE +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSES/fastembed-NOTICE +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSES/onnxruntime-LICENSE +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/LICENSES/paraphrase-multilingual-MiniLM-L12-v2-LICENSE +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/README.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/hatch_build.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/pyproject.toml +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/__main__.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/_excludes.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/_terminal.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/adapters.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_ask.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_doctor.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_init.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_list.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_plan.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_recall.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/cli_update.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/embedding.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/mcp_server.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/migrate.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/migrations/001_initial.sql +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/migrations/README.md +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/migrations/__init__.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/paths.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/plan_validator.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/platforms.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/question.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/recall_chunker.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/src/c3/recall_index.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/__init__.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/conftest.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/__init__.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_check_agent_invocation.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_consolidate_memory.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_hook_utils.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_permission_handler.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_permission_handler_toast.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_pip_reinstall_reminder.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_planner_check.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_planner_check_dev.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_post_tool.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_pre_tool.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_recall_inject.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_record_review_decision.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_restore_session.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_review_hint_inject.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_select_tier_escalation.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_session_stop.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_session_utils.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_settings_local_absolute_paths.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_similarity_boost.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_statusline.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_statusline_template_sync.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_sync_check.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/hooks/test_template_guard.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/__init__.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/_skill_helpers.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_dev_workflow_no_task_type.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_init_session_no_task_type.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_planner_lightweight.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_recall_skill.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_session_backlog_reconciliation.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_setup_templates.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_start_skill_bugfix_flow.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_start_skill_new_flow.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/skills/test_start_skill_security_audit_phase.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_adapters.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_ask.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_entry.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_init.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_list.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_plan.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_recall.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_update_breaking_changes.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_cli_update_deletions.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_docstring_consistency.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_embedding.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_excludes.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_extract_breaking_changes.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_mcp_server_elicit.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_paths.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_plan_validator.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_pre_compact.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_pre_tool_hook.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_precompact_additional.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_precompact_toctou_fixes.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_recall_chunker.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_recall_index.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_references_migration.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_session_utils_additional.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_skill_no_builtin_conflict.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_statusline.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_stop_additional.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_stop_hook.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_stop_precompact_fixes.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_sync_template_stop.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_template_pre_tool_hook.py +0 -0
- {claude_code_conductor-2.20.0 → claude_code_conductor-2.22.0}/tests/test_worktree_guard.py +0 -0
|
@@ -323,6 +323,7 @@ def write_tier_selection(
|
|
|
323
323
|
escalation_reason: str | None = None,
|
|
324
324
|
prompt_prefix: str | None = None,
|
|
325
325
|
prompt_hash: str | None = None,
|
|
326
|
+
session_id: str | None = None,
|
|
326
327
|
) -> None:
|
|
327
328
|
"""直近の選択結果を ``tier_selection.json`` に書く。
|
|
328
329
|
|
|
@@ -335,6 +336,9 @@ def write_tier_selection(
|
|
|
335
336
|
|
|
336
337
|
``escalated`` / ``escalation_reason`` を任意で含める。
|
|
337
338
|
failure rate に基づく昇格が起きた場合のみ True / 文字列が入る。
|
|
339
|
+
|
|
340
|
+
``session_id`` を任意で含める。UserPromptSubmit payload の session UUID。
|
|
341
|
+
None のときは tier_selection.json のキー自体を省略する(後方互換)。
|
|
338
342
|
"""
|
|
339
343
|
os.makedirs(os.path.dirname(TIER_SELECTION_PATH), exist_ok=True)
|
|
340
344
|
payload: dict[str, object] = {
|
|
@@ -354,6 +358,8 @@ def write_tier_selection(
|
|
|
354
358
|
payload["prompt_prefix"] = prompt_prefix
|
|
355
359
|
if prompt_hash is not None:
|
|
356
360
|
payload["prompt_hash"] = prompt_hash
|
|
361
|
+
if session_id is not None:
|
|
362
|
+
payload["session_id"] = session_id
|
|
357
363
|
try:
|
|
358
364
|
with open(TIER_SELECTION_PATH, "w", encoding="utf-8") as f:
|
|
359
365
|
json.dump(payload, f, ensure_ascii=False)
|
|
@@ -415,6 +421,8 @@ def main() -> int:
|
|
|
415
421
|
if not isinstance(prompt, str) or not prompt.strip():
|
|
416
422
|
return 0
|
|
417
423
|
|
|
424
|
+
session_id = payload.get("session_id")
|
|
425
|
+
|
|
418
426
|
# Phase 2-C: 類似度推定で complexity を補強
|
|
419
427
|
heuristic_complexity = estimate_complexity(prompt)
|
|
420
428
|
strong_complexity, weak_matches = similarity_boost(prompt)
|
|
@@ -448,6 +456,7 @@ def main() -> int:
|
|
|
448
456
|
escalated=escalated, escalation_reason=escalation_reason,
|
|
449
457
|
prompt_prefix=prompt_prefix,
|
|
450
458
|
prompt_hash=prompt_hash,
|
|
459
|
+
session_id=session_id,
|
|
451
460
|
)
|
|
452
461
|
|
|
453
462
|
context_text = build_additional_context(
|
|
@@ -89,6 +89,23 @@ def main() -> int:
|
|
|
89
89
|
except Exception as e:
|
|
90
90
|
print(f"[session_stop:consolidate_memory] failed: {e}", file=sys.stderr)
|
|
91
91
|
|
|
92
|
+
# Phase 3: usage_ingester — セッションログからコスト集計データを ingestion
|
|
93
|
+
# worktree session では起動しない(stop.run の is_worktree 判定と整合)
|
|
94
|
+
# 安全側動作: session_utils ロード失敗時や is_worktree 判定失敗時は ingester をスキップする。
|
|
95
|
+
# worktree 判定不能で誤起動するより skip を優先(except Exception で捕捉 → 早期終了)。
|
|
96
|
+
try:
|
|
97
|
+
session_utils_module = _load_module("session_utils")
|
|
98
|
+
if not session_utils_module.is_worktree(os.getcwd()):
|
|
99
|
+
session_id = payload.get("session_id")
|
|
100
|
+
transcript_path = payload.get("transcript_path")
|
|
101
|
+
if session_id and transcript_path:
|
|
102
|
+
from pathlib import Path as _Path # noqa: PLC0415
|
|
103
|
+
from c3.usage_ingester import ingest_session # noqa: PLC0415
|
|
104
|
+
project_dir = _Path(transcript_path).parent
|
|
105
|
+
ingest_session(session_id=session_id, project_dir=project_dir)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"[session_stop:usage_ingester] failed: {type(e).__name__}", file=sys.stderr)
|
|
108
|
+
|
|
92
109
|
return 0
|
|
93
110
|
|
|
94
111
|
|
|
@@ -210,15 +210,19 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
210
210
|
# Phase 2-B: tier_recent_outcomes にも 1 件記録(escalation 判定の母数)。
|
|
211
211
|
# tier_bandit 更新が失敗してもこちらは試みる(DB が一時的に詰まる程度なら
|
|
212
212
|
# 片方だけ通る可能性もある)。
|
|
213
|
+
# v2.22.0: session_id を渡して cost 紐づけを可能にする。
|
|
214
|
+
# session_id キーが無い古い tier_selection.json の場合は None になる(後方互換)。
|
|
215
|
+
session_id = selection.get("session_id")
|
|
213
216
|
try:
|
|
214
217
|
c3_db.record_tier_recent_outcome(
|
|
215
218
|
complexity=selection["complexity"],
|
|
216
219
|
tier=selection["tier"],
|
|
217
220
|
success=success,
|
|
221
|
+
session_id=session_id,
|
|
218
222
|
)
|
|
219
223
|
except Exception as exc: # noqa: BLE001
|
|
220
224
|
print(
|
|
221
|
-
f"[record_tier_outcome] tier_recent_outcomes record skipped: {exc}",
|
|
225
|
+
f"[record_tier_outcome] tier_recent_outcomes record skipped: {type(exc).__name__}",
|
|
222
226
|
file=sys.stderr,
|
|
223
227
|
)
|
|
224
228
|
|
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.22.0] - 2026-05-25
|
|
4
|
+
|
|
5
|
+
**tier-routing cost 紐づけデータ蓄積**: tier_recent_outcomes に session_id 列を追加し、agent_cost_runs と JOIN できるデータ基盤を整備する。`c3 tier stats` に complexity×tier 別の平均コストセクションを追加する。cost-aware routing 本体は v2.23.0。
|
|
6
|
+
|
|
7
|
+
**スコープ注記**: 単価テーブル自動更新は公式 price API が非提供のため pricing.py の手動メンテ継続。tier_bandit の total_cost_usd / cost_samples 列は v2.23.0 用に確保のみ(書き込み・読み出しなし)。
|
|
8
|
+
|
|
9
|
+
### 機能追加
|
|
10
|
+
|
|
11
|
+
- **`src/c3/migrations/003_tier_cost.sql`(新規)**: `tier_recent_outcomes` に `session_id TEXT` 列と `idx_tier_recent_session` インデックスを追加。`tier_bandit` に `total_cost_usd REAL DEFAULT 0.0` / `cost_samples INTEGER DEFAULT 0` 列を追加(v2.23.0 用確保のみ)。既存データは ADD COLUMN DEFAULT で保持(破壊的変更なし)。
|
|
12
|
+
|
|
13
|
+
- **`src/c3/db.py`: `record_tier_recent_outcome` に `session_id` 追加**: kw-only 引数 `session_id: str | None = None` を追加。既存呼び出しは後方互換(省略時 NULL 保存)。
|
|
14
|
+
|
|
15
|
+
- **`src/c3/db.py`: `read_tier_cost_summary` 追加**: tier_recent_outcomes × agent_cost_runs を session_id で JOIN し、complexity×tier 別の sessions / total_cost_usd / avg_cost_usd を返す。2 段 CTE で session コストの 1 session 内重複計上を防ぐ(mainline 除外・session 単位 SUM → DISTINCT JOIN)。テーブル不在・データ不在・session_id 全 NULL で `[]`。
|
|
16
|
+
|
|
17
|
+
- **`.claude/hooks/select_tier.py`: session_id 記録**: UserPromptSubmit payload から session_id を取得し tier_selection.json に追記。session_id が None のときはキーを省略(既存テスト互換)。
|
|
18
|
+
|
|
19
|
+
- **`.claude/skills/dev-workflow/scripts/record_tier_outcome.py`: session_id 受け渡し**: tier_selection.json から session_id を読み `record_tier_recent_outcome` に渡す。session_id キーが無い古い json でも動作(None として扱う)。
|
|
20
|
+
|
|
21
|
+
- **`c3 tier stats` に Tier 別平均コストセクション追加**: `_collect_snapshot()` に `read_tier_cost_summary()` の結果を `tier_cost` キーで追加。human 表示に「Tier 別平均コスト(粗い概算 / 精度向上は v2.23.0)」セクションを追加。tier_cost が空のときは「(cost 紐づけデータ未収集)」と表示。`--json` 出力の `tier_cost` キーにも自動反映。
|
|
22
|
+
|
|
23
|
+
### 変更
|
|
24
|
+
|
|
25
|
+
- **`src/c3/__init__.py`**: `__version__` を `"2.21.0"` から `"2.22.0"` に更新。
|
|
26
|
+
|
|
27
|
+
- **注記更新**: `c3 tier stats` の「cost-aware routing は v2.22.0 予定」を「データ紐づけ蓄積。cost-aware routing 本体は v2.23.0 予定」へ更新。
|
|
28
|
+
|
|
29
|
+
## [2.21.0] - 2026-05-25
|
|
30
|
+
|
|
31
|
+
**tier-routing コスト統合(データ収集基盤)**: Claude Code セッションログ(`~/.claude/projects/<slug>/<session>.jsonl` + subagent jsonl)を読み込み、モデル単価で USD 換算して c3.db に蓄積するデータ収集基盤を整備する。将来の cost-aware routing(v2.22.0)の土台。
|
|
32
|
+
|
|
33
|
+
**スコープ注記**: 本リリースはデータ収集基盤のみ。cost-aware routing と tier_bandit のコスト列は v2.22.0 予定。
|
|
34
|
+
|
|
35
|
+
### 機能追加
|
|
36
|
+
|
|
37
|
+
- **`src/c3/pricing.py`(新規)**: Claude API モデルの USD/MTok 単価から token コストを計算する純関数モジュール。
|
|
38
|
+
`resolve_tier(model)`・`compute_cost_usd(...) -> tuple[float, bool]`・`known_models()` を提供。
|
|
39
|
+
Opus は世代で単価が 3 倍異なる(4.1/4=$15 系、4.5/4.6/4.7=$5 系)ため、具体パターン優先マッチ → tier 部分一致 fallback の 2 段構成を採用。
|
|
40
|
+
単価は 2026-05-25 公式取得値(出典 URL を docstring に明記)。
|
|
41
|
+
|
|
42
|
+
- **`src/c3/migrations/002_agent_cost_runs.sql`(新規)**: `agent_cost_runs` テーブル・`usage_ingest_state` テーブル・インデックスを追加する migration。
|
|
43
|
+
PK=(session_id, agent_id, model) で「1 エージェント × 1 モデル = 1 行」の集約設計。
|
|
44
|
+
既存 event-based `agent_runs`(001)は一切変更しない。
|
|
45
|
+
|
|
46
|
+
- **`src/c3/usage_ingester.py`(新規)**: セッションログ取り込みモジュール。
|
|
47
|
+
公開 API `ingest_session(*, session_id, project_dir, db_path=None) -> IngestResult`。
|
|
48
|
+
mainline / subagent jsonl を走査し、model 単位でトークンを合算して `insert_agent_cost_run` で upsert する。
|
|
49
|
+
session_id UUID validate・パストラバーサル防止・symlink スキップ・例外 type 名のみログ(SR-R-001 準拠)。
|
|
50
|
+
|
|
51
|
+
- **`.claude/hooks/session_stop.py` Phase 3 追加**: セッション終了時に `ingest_session` を呼ぶ Phase 3 を追加。
|
|
52
|
+
worktree session では起動しない。例外握りつぶしで exit 0 を維持する。
|
|
53
|
+
|
|
54
|
+
- **`c3 tier stats` の Agent 別コスト集計セクション追加**: `_collect_snapshot()` に `read_agent_cost_summary()` を追加し、human / JSON 両出力に `agent_cost` セクションを表示する。
|
|
55
|
+
mainline 行には「(マクロ集計・tier 学習対象外)」を明示。0 件のときは「(コストデータ未収集)」を表示。
|
|
56
|
+
|
|
57
|
+
### 変更
|
|
58
|
+
|
|
59
|
+
- **`src/c3/db.py`**: 4 ヘルパー追加(`insert_agent_cost_run` / `read_agent_cost_summary` / `get_ingest_offset` / `set_ingest_offset`)。既存 tier ヘルパー規約(DB 不在で静かに False/0/[]・WAL・busy_timeout)に準拠。
|
|
60
|
+
|
|
61
|
+
- **`src/c3/__init__.py`**: `__version__` を `"2.20.0"` から `"2.21.0"` に更新。
|
|
62
|
+
|
|
3
63
|
## [2.20.0] - 2026-05-25
|
|
4
64
|
|
|
5
65
|
**SQLite schema migration 枠組み導入**: `.claude/hooks/schema.sql` の「冪等 DDL 一発実行」から、`src/c3/migrations/` の「連番 NNN_xxx.sql migration runner」へ移行する基盤を整備する。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-conductor
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.22.0
|
|
4
4
|
Summary: Multi-agent orchestration framework for Claude Code with Codex/Cursor adapters (C3)
|
|
5
5
|
Project-URL: Homepage, https://github.com/satoh-y-0323/claude-code-conductor
|
|
6
6
|
Project-URL: Repository, https://github.com/satoh-y-0323/claude-code-conductor
|
|
@@ -89,7 +89,7 @@ def handle_stats(args: argparse.Namespace) -> int:
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
|
|
92
|
-
"""DB から tier_bandit / tier_recent_outcomes を読み snapshot dict を返す。"""
|
|
92
|
+
"""DB から tier_bandit / tier_recent_outcomes / agent_cost / tier_cost を読み snapshot dict を返す。"""
|
|
93
93
|
bandit_rows: list[dict[str, Any]] = []
|
|
94
94
|
total_trials = 0
|
|
95
95
|
|
|
@@ -114,6 +114,10 @@ def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
|
|
|
114
114
|
db_path=db_path,
|
|
115
115
|
)
|
|
116
116
|
|
|
117
|
+
agent_cost: list[dict[str, Any]] = c3_db.read_agent_cost_summary(db_path=db_path)
|
|
118
|
+
|
|
119
|
+
tier_cost: list[dict[str, Any]] = c3_db.read_tier_cost_summary(db_path=db_path)
|
|
120
|
+
|
|
117
121
|
if total_trials < _LEARNING_THRESHOLD:
|
|
118
122
|
mode = "uniform"
|
|
119
123
|
else:
|
|
@@ -127,6 +131,8 @@ def _collect_snapshot(db_path, recent_limit: int) -> dict[str, Any]:
|
|
|
127
131
|
},
|
|
128
132
|
"tier_bandit": bandit_rows,
|
|
129
133
|
"recent_outcomes": recent_outcomes,
|
|
134
|
+
"agent_cost": agent_cost,
|
|
135
|
+
"tier_cost": tier_cost,
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
|
|
@@ -166,3 +172,40 @@ def _render_human(snapshot: dict[str, Any]) -> None:
|
|
|
166
172
|
print("== 学習データ記録チャネル ==")
|
|
167
173
|
print("記録元: dev-workflow フェーズ E の最終承認時のみ(record_tier_outcome.py)")
|
|
168
174
|
print("直接指示作業ではデータが溜まりません(設計通り)")
|
|
175
|
+
print()
|
|
176
|
+
|
|
177
|
+
print("== Agent 別コスト集計(agent_cost_runs) ==")
|
|
178
|
+
agent_cost = snapshot.get("agent_cost", [])
|
|
179
|
+
if not agent_cost:
|
|
180
|
+
print("(コストデータ未収集)")
|
|
181
|
+
else:
|
|
182
|
+
print(
|
|
183
|
+
f"{'agent_type':<16} {'runs':>5} {'total_usd':>10} "
|
|
184
|
+
f"{'in_tok':>9} {'out_tok':>9} {'cache_r':>9} {'cache_w':>9}"
|
|
185
|
+
)
|
|
186
|
+
for row in agent_cost:
|
|
187
|
+
note = " (マクロ集計・tier 学習対象外)" if row["agent_type"] == "mainline" else ""
|
|
188
|
+
print(
|
|
189
|
+
f"{row['agent_type']:<16} {row['runs']:>5} "
|
|
190
|
+
f"${row['total_cost_usd']:>9.4f} "
|
|
191
|
+
f"{row['input_tokens']:>9} {row['output_tokens']:>9} "
|
|
192
|
+
f"{row['cache_read_tokens']:>9} {row['cache_create_tokens']:>9}"
|
|
193
|
+
f"{note}"
|
|
194
|
+
)
|
|
195
|
+
print()
|
|
196
|
+
|
|
197
|
+
print("== Tier 別平均コスト(粗い概算 / 精度向上は v2.23.0) ==")
|
|
198
|
+
tier_cost = snapshot.get("tier_cost", [])
|
|
199
|
+
if not tier_cost:
|
|
200
|
+
print("(cost 紐づけデータ未収集)")
|
|
201
|
+
else:
|
|
202
|
+
print(f"{'complexity':<12} {'tier':<8} {'sessions':>8} {'avg_usd':>10} {'total_usd':>10}")
|
|
203
|
+
for row in tier_cost:
|
|
204
|
+
print(
|
|
205
|
+
f"{row['complexity']:<12} {row['tier']:<8} "
|
|
206
|
+
f"{row['sessions']:>8} "
|
|
207
|
+
f"${row['avg_cost_usd']:>9.4f} "
|
|
208
|
+
f"${row['total_cost_usd']:>9.4f}"
|
|
209
|
+
)
|
|
210
|
+
print()
|
|
211
|
+
print("(注: データ紐づけ蓄積。cost-aware routing 本体は v2.23.0 予定)")
|
|
@@ -358,12 +358,21 @@ def record_tier_recent_outcome(
|
|
|
358
358
|
tier: str,
|
|
359
359
|
success: bool,
|
|
360
360
|
db_path: Path | None = None,
|
|
361
|
+
session_id: str | None = None,
|
|
361
362
|
) -> bool:
|
|
362
363
|
"""``tier_recent_outcomes`` に 1 件 INSERT する。
|
|
363
364
|
|
|
364
365
|
Phase 2-B のエスカレーション判定用。tier_bandit の累積 α/β とは別に、
|
|
365
366
|
直近 N 件の event を時系列で保持する。
|
|
366
367
|
|
|
368
|
+
Args:
|
|
369
|
+
complexity: 'simple' | 'medium' | 'complex'。
|
|
370
|
+
tier: 'haiku' | 'sonnet' | 'opus'。
|
|
371
|
+
success: True なら成功、False なら失敗。
|
|
372
|
+
db_path: c3.db のパス。省略時は :func:`locate_c3_db` で探索。
|
|
373
|
+
session_id: セッション UUID(v2.22.0+)。省略時は NULL で保存。
|
|
374
|
+
cost 紐づけに使用。None の場合は既存行との後方互換を維持する。
|
|
375
|
+
|
|
367
376
|
Returns:
|
|
368
377
|
INSERT 成功時 True、DB 不在 / エラー時 False。
|
|
369
378
|
"""
|
|
@@ -382,15 +391,15 @@ def record_tier_recent_outcome(
|
|
|
382
391
|
_apply_busy_timeout(conn)
|
|
383
392
|
conn.execute(
|
|
384
393
|
"INSERT INTO tier_recent_outcomes "
|
|
385
|
-
"(task_complexity, tier, success, ts) VALUES (?, ?, ?, ?)",
|
|
386
|
-
(complexity, tier, 1 if success else 0, now_iso),
|
|
394
|
+
"(task_complexity, tier, success, ts, session_id) VALUES (?, ?, ?, ?, ?)",
|
|
395
|
+
(complexity, tier, 1 if success else 0, now_iso, session_id),
|
|
387
396
|
)
|
|
388
397
|
conn.commit()
|
|
389
398
|
finally:
|
|
390
399
|
conn.close()
|
|
391
400
|
return True
|
|
392
401
|
except Exception as exc: # noqa: BLE001
|
|
393
|
-
logger.warning("failed to record tier_recent_outcome: %s", exc)
|
|
402
|
+
logger.warning("failed to record tier_recent_outcome: %s", type(exc).__name__)
|
|
394
403
|
return False
|
|
395
404
|
|
|
396
405
|
|
|
@@ -493,3 +502,343 @@ def read_tier_failure_rate(
|
|
|
493
502
|
|
|
494
503
|
failures = sum(1 for r in rows if r[0] == 0)
|
|
495
504
|
return failures / sample_count, sample_count
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ---------------------------------------------------------------------------
|
|
508
|
+
# usage-ingester: agent_cost_runs / usage_ingest_state ヘルパー(v2.21.0)
|
|
509
|
+
# ---------------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def insert_agent_cost_run(
|
|
513
|
+
*,
|
|
514
|
+
session_id: str,
|
|
515
|
+
agent_id: str,
|
|
516
|
+
agent_type: str,
|
|
517
|
+
description: str | None,
|
|
518
|
+
model: str,
|
|
519
|
+
attribution_skill: str | None,
|
|
520
|
+
input_tokens: int,
|
|
521
|
+
output_tokens: int,
|
|
522
|
+
cache_read_tokens: int,
|
|
523
|
+
cache_create_tokens: int,
|
|
524
|
+
total_cost_usd: float,
|
|
525
|
+
db_path: Path | None = None,
|
|
526
|
+
) -> bool:
|
|
527
|
+
"""agent_cost_runs に 1 行 upsert する。
|
|
528
|
+
|
|
529
|
+
PK=(session_id, agent_id, model) で「1 エージェント × 1 モデル = 1 行」。
|
|
530
|
+
同一 PK が既に存在する場合は全数値列・total_cost_usd・recorded_at を最新合算値に上書き。
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
session_id: セッション UUID。
|
|
534
|
+
agent_id: 'agent-<id>' または 'mainline'。
|
|
535
|
+
agent_type: meta.json の agentType / 'mainline'。
|
|
536
|
+
description: meta.json の description(任意)。
|
|
537
|
+
model: message.model 文字列。
|
|
538
|
+
attribution_skill: assistant レコードの attributionSkill(任意)。
|
|
539
|
+
input_tokens: 入力トークン数(jsonl 内合算値)。
|
|
540
|
+
output_tokens: 出力トークン数(jsonl 内合算値)。
|
|
541
|
+
cache_read_tokens: キャッシュ読み込みトークン数(jsonl 内合算値)。
|
|
542
|
+
cache_create_tokens: キャッシュ書き込みトークン数(jsonl 内合算値)。
|
|
543
|
+
total_cost_usd: compute_cost_usd が返した USD コスト。
|
|
544
|
+
db_path: c3.db のパス。省略時は locate_c3_db() で探索。
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
upsert 成功時 True、DB 不在 / sqlite3.Error 時は静かに False。
|
|
548
|
+
"""
|
|
549
|
+
if db_path is None:
|
|
550
|
+
db_path = locate_c3_db()
|
|
551
|
+
if db_path is None:
|
|
552
|
+
return False
|
|
553
|
+
|
|
554
|
+
from datetime import timezone as _tz # noqa: PLC0415
|
|
555
|
+
now_iso = datetime.now(_tz.utc).isoformat(timespec="seconds")
|
|
556
|
+
|
|
557
|
+
try:
|
|
558
|
+
conn = sqlite3.connect(str(db_path))
|
|
559
|
+
try:
|
|
560
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
561
|
+
_apply_busy_timeout(conn)
|
|
562
|
+
conn.execute(
|
|
563
|
+
"INSERT INTO agent_cost_runs "
|
|
564
|
+
"(session_id, agent_id, agent_type, description, model, "
|
|
565
|
+
" attribution_skill, input_tokens, output_tokens, "
|
|
566
|
+
" cache_read_tokens, cache_create_tokens, "
|
|
567
|
+
" total_cost_usd, recorded_at) "
|
|
568
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
|
569
|
+
"ON CONFLICT(session_id, agent_id, model) DO UPDATE SET "
|
|
570
|
+
" agent_type = excluded.agent_type, "
|
|
571
|
+
" description = excluded.description, "
|
|
572
|
+
" attribution_skill = excluded.attribution_skill, "
|
|
573
|
+
" input_tokens = excluded.input_tokens, "
|
|
574
|
+
" output_tokens = excluded.output_tokens, "
|
|
575
|
+
" cache_read_tokens = excluded.cache_read_tokens, "
|
|
576
|
+
" cache_create_tokens = excluded.cache_create_tokens, "
|
|
577
|
+
" total_cost_usd = excluded.total_cost_usd, "
|
|
578
|
+
" recorded_at = excluded.recorded_at",
|
|
579
|
+
(
|
|
580
|
+
session_id, agent_id, agent_type, description, model,
|
|
581
|
+
attribution_skill, input_tokens, output_tokens,
|
|
582
|
+
cache_read_tokens, cache_create_tokens,
|
|
583
|
+
total_cost_usd, now_iso,
|
|
584
|
+
),
|
|
585
|
+
)
|
|
586
|
+
conn.commit()
|
|
587
|
+
finally:
|
|
588
|
+
conn.close()
|
|
589
|
+
return True
|
|
590
|
+
except Exception as exc: # noqa: BLE001
|
|
591
|
+
logger.warning("failed to insert_agent_cost_run: %s", type(exc).__name__)
|
|
592
|
+
return False
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def read_agent_cost_summary(
|
|
596
|
+
*,
|
|
597
|
+
db_path: Path | None = None,
|
|
598
|
+
limit: int = 50,
|
|
599
|
+
) -> list[dict]:
|
|
600
|
+
"""agent_cost_runs を agent_type 別に集計した結果を返す。
|
|
601
|
+
|
|
602
|
+
SELECT agent_type, COUNT(*) runs, SUM(total_cost_usd), SUM(input_tokens),
|
|
603
|
+
SUM(output_tokens), SUM(cache_read_tokens), SUM(cache_create_tokens)
|
|
604
|
+
FROM agent_cost_runs GROUP BY agent_type ORDER BY total_cost_usd DESC LIMIT ?
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
db_path: c3.db のパス。省略時は locate_c3_db() で探索。
|
|
608
|
+
limit: 返す最大件数(デフォルト 50)。
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
各行を dict にしたリスト。キー:
|
|
612
|
+
``agent_type`` / ``runs`` / ``total_cost_usd`` /
|
|
613
|
+
``input_tokens`` / ``output_tokens`` /
|
|
614
|
+
``cache_read_tokens`` / ``cache_create_tokens``。
|
|
615
|
+
DB 不在 / テーブル不在 / エラー時は空リスト。
|
|
616
|
+
"""
|
|
617
|
+
if db_path is None:
|
|
618
|
+
db_path = locate_c3_db()
|
|
619
|
+
if db_path is None:
|
|
620
|
+
return []
|
|
621
|
+
|
|
622
|
+
try:
|
|
623
|
+
conn = sqlite3.connect(str(db_path))
|
|
624
|
+
try:
|
|
625
|
+
# WAL は書き込みヘルパー呼び出し時または migrate 時に設定済みの前提
|
|
626
|
+
# (既存 read_recent_outcomes 等の read ヘルパーと同方針 / CR-M-001)
|
|
627
|
+
_apply_busy_timeout(conn)
|
|
628
|
+
rows = conn.execute(
|
|
629
|
+
"SELECT agent_type, "
|
|
630
|
+
" COUNT(*) AS runs, "
|
|
631
|
+
" SUM(total_cost_usd) AS total_cost_usd, "
|
|
632
|
+
" SUM(input_tokens) AS input_tokens, "
|
|
633
|
+
" SUM(output_tokens) AS output_tokens, "
|
|
634
|
+
" SUM(cache_read_tokens) AS cache_read_tokens, "
|
|
635
|
+
" SUM(cache_create_tokens) AS cache_create_tokens "
|
|
636
|
+
"FROM agent_cost_runs "
|
|
637
|
+
"GROUP BY agent_type "
|
|
638
|
+
"ORDER BY total_cost_usd DESC "
|
|
639
|
+
"LIMIT ?",
|
|
640
|
+
(limit,),
|
|
641
|
+
).fetchall()
|
|
642
|
+
finally:
|
|
643
|
+
conn.close()
|
|
644
|
+
except sqlite3.OperationalError as exc:
|
|
645
|
+
# テーブル不在(no such table)は [] を返す(DB 未初期化でも止めない)
|
|
646
|
+
logger.debug(
|
|
647
|
+
"read_agent_cost_summary: table not found or inaccessible: %s",
|
|
648
|
+
type(exc).__name__,
|
|
649
|
+
)
|
|
650
|
+
return []
|
|
651
|
+
except Exception as exc: # noqa: BLE001
|
|
652
|
+
logger.warning("read_agent_cost_summary: unexpected error: %s", type(exc).__name__)
|
|
653
|
+
return []
|
|
654
|
+
|
|
655
|
+
return [
|
|
656
|
+
{
|
|
657
|
+
"agent_type": row[0],
|
|
658
|
+
"runs": row[1],
|
|
659
|
+
"total_cost_usd": row[2],
|
|
660
|
+
"input_tokens": row[3],
|
|
661
|
+
"output_tokens": row[4],
|
|
662
|
+
"cache_read_tokens": row[5],
|
|
663
|
+
"cache_create_tokens": row[6],
|
|
664
|
+
}
|
|
665
|
+
for row in rows
|
|
666
|
+
]
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def read_tier_cost_summary(
|
|
670
|
+
*,
|
|
671
|
+
db_path: Path | None = None,
|
|
672
|
+
) -> list[dict]:
|
|
673
|
+
"""tier_recent_outcomes × agent_cost_runs を session_id で JOIN し、
|
|
674
|
+
complexity×tier 別コストを返す(粗い概算 / 精度向上は v2.23.0)。
|
|
675
|
+
|
|
676
|
+
2 段 CTE で重複計上を防ぐ:
|
|
677
|
+
- ``session_cost`` CTE: agent_cost_runs を mainline 除外しつつ
|
|
678
|
+
session_id 単位で SUM(1 session = 1 行)→ cost の重複計上を構造的に防ぐ
|
|
679
|
+
- ``outcome_sessions`` CTE: tier_recent_outcomes を
|
|
680
|
+
(session_id, task_complexity, tier) で DISTINCT 化。
|
|
681
|
+
session_id が NULL(v2.22.0 移行前行)は除外
|
|
682
|
+
- JOIN して complexity×tier 別に sessions / total_cost_usd / avg_cost_usd を算出
|
|
683
|
+
|
|
684
|
+
既知の不正確性(v2.22.0 許容・精度向上は v2.23.0):
|
|
685
|
+
- session が複数の (complexity, tier) outcome を持つ場合、同一 session の cost が
|
|
686
|
+
複数の (complexity,tier) 行に重複加算されうる(1 session 内での cost 重複は防ぐが、
|
|
687
|
+
複数 outcome を持つ session の cross-outcome 重複は未解決)
|
|
688
|
+
- cost は session 全体の non-mainline 合計であり、特定 tier の model 行に限定しない
|
|
689
|
+
- agent_id 単位の紐づけはしていない
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
db_path: c3.db のパス。省略時は :func:`locate_c3_db` で探索。
|
|
693
|
+
|
|
694
|
+
Returns:
|
|
695
|
+
各行を dict にしたリスト。キー:
|
|
696
|
+
``complexity``(str) / ``tier``(str) / ``sessions``(int) /
|
|
697
|
+
``total_cost_usd``(float) / ``avg_cost_usd``(float)。
|
|
698
|
+
ORDER BY total_cost_usd DESC。
|
|
699
|
+
テーブル不在 / データ不在 / session_id 全 NULL / JOIN 0 行 /
|
|
700
|
+
DB 不在 / エラー時は空リストを返す。
|
|
701
|
+
"""
|
|
702
|
+
if db_path is None:
|
|
703
|
+
db_path = locate_c3_db()
|
|
704
|
+
if db_path is None:
|
|
705
|
+
return []
|
|
706
|
+
|
|
707
|
+
try:
|
|
708
|
+
conn = sqlite3.connect(str(db_path))
|
|
709
|
+
try:
|
|
710
|
+
# WAL は書き込みヘルパー呼び出し時または migrate 時に設定済みの前提
|
|
711
|
+
# (read_agent_cost_summary 等の read ヘルパーと同方針 / CR-M-001)
|
|
712
|
+
_apply_busy_timeout(conn)
|
|
713
|
+
rows = conn.execute(
|
|
714
|
+
"WITH session_cost AS ("
|
|
715
|
+
" SELECT session_id,"
|
|
716
|
+
" SUM(total_cost_usd) AS session_cost_usd"
|
|
717
|
+
" FROM agent_cost_runs"
|
|
718
|
+
" WHERE agent_type <> 'mainline'"
|
|
719
|
+
" GROUP BY session_id"
|
|
720
|
+
"),"
|
|
721
|
+
"outcome_sessions AS ("
|
|
722
|
+
" SELECT DISTINCT session_id, task_complexity, tier"
|
|
723
|
+
" FROM tier_recent_outcomes"
|
|
724
|
+
" WHERE session_id IS NOT NULL"
|
|
725
|
+
") "
|
|
726
|
+
"SELECT o.task_complexity AS complexity,"
|
|
727
|
+
" o.tier AS tier,"
|
|
728
|
+
" COUNT(DISTINCT o.session_id) AS sessions,"
|
|
729
|
+
" SUM(sc.session_cost_usd) AS total_cost_usd,"
|
|
730
|
+
" SUM(sc.session_cost_usd) / COUNT(DISTINCT o.session_id)"
|
|
731
|
+
" AS avg_cost_usd "
|
|
732
|
+
"FROM outcome_sessions o "
|
|
733
|
+
"JOIN session_cost sc ON sc.session_id = o.session_id "
|
|
734
|
+
"GROUP BY o.task_complexity, o.tier "
|
|
735
|
+
"ORDER BY total_cost_usd DESC"
|
|
736
|
+
).fetchall()
|
|
737
|
+
finally:
|
|
738
|
+
conn.close()
|
|
739
|
+
except sqlite3.OperationalError as exc:
|
|
740
|
+
# テーブル不在(no such table)は [] を返す(DB 未初期化でも止めない)
|
|
741
|
+
logger.debug(
|
|
742
|
+
"read_tier_cost_summary: table not found or inaccessible: %s",
|
|
743
|
+
type(exc).__name__,
|
|
744
|
+
)
|
|
745
|
+
return []
|
|
746
|
+
except Exception as exc: # noqa: BLE001
|
|
747
|
+
logger.warning("read_tier_cost_summary: unexpected error: %s", type(exc).__name__)
|
|
748
|
+
return []
|
|
749
|
+
|
|
750
|
+
return [
|
|
751
|
+
{
|
|
752
|
+
"complexity": row[0],
|
|
753
|
+
"tier": row[1],
|
|
754
|
+
"sessions": int(row[2]),
|
|
755
|
+
"total_cost_usd": float(row[3]) if row[3] is not None else 0.0,
|
|
756
|
+
"avg_cost_usd": float(row[4]) if row[4] is not None else 0.0,
|
|
757
|
+
}
|
|
758
|
+
for row in rows
|
|
759
|
+
]
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def get_ingest_offset(
|
|
763
|
+
file_key: str,
|
|
764
|
+
*,
|
|
765
|
+
db_path: Path | None = None,
|
|
766
|
+
) -> int:
|
|
767
|
+
"""usage_ingest_state から処理済み行数(offset)を返す。
|
|
768
|
+
|
|
769
|
+
Args:
|
|
770
|
+
file_key: '<session>:mainline' / '<session>:agent-<id>'。
|
|
771
|
+
db_path: c3.db のパス。省略時は locate_c3_db() で探索。
|
|
772
|
+
|
|
773
|
+
Returns:
|
|
774
|
+
処理済み行数。行・テーブル不在・DB 不在は 0 を返す。
|
|
775
|
+
"""
|
|
776
|
+
if db_path is None:
|
|
777
|
+
db_path = locate_c3_db()
|
|
778
|
+
if db_path is None:
|
|
779
|
+
return 0
|
|
780
|
+
|
|
781
|
+
try:
|
|
782
|
+
conn = sqlite3.connect(str(db_path))
|
|
783
|
+
try:
|
|
784
|
+
# WAL は書き込みヘルパー呼び出し時または migrate 時に設定済みの前提
|
|
785
|
+
# (既存 read_recent_outcomes 等の read ヘルパーと同方針 / CR-M-001)
|
|
786
|
+
_apply_busy_timeout(conn)
|
|
787
|
+
row = conn.execute(
|
|
788
|
+
"SELECT last_offset FROM usage_ingest_state WHERE file_key = ?",
|
|
789
|
+
(file_key,),
|
|
790
|
+
).fetchone()
|
|
791
|
+
finally:
|
|
792
|
+
conn.close()
|
|
793
|
+
except Exception as exc: # noqa: BLE001
|
|
794
|
+
logger.debug("get_ingest_offset: %s", type(exc).__name__)
|
|
795
|
+
return 0
|
|
796
|
+
|
|
797
|
+
return row[0] if row is not None else 0
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def set_ingest_offset(
|
|
801
|
+
file_key: str,
|
|
802
|
+
offset: int,
|
|
803
|
+
*,
|
|
804
|
+
db_path: Path | None = None,
|
|
805
|
+
) -> bool:
|
|
806
|
+
"""usage_ingest_state に offset を upsert する。
|
|
807
|
+
|
|
808
|
+
Args:
|
|
809
|
+
file_key: '<session>:mainline' / '<session>:agent-<id>'。
|
|
810
|
+
offset: 処理済み行数(= 次回開始行)。
|
|
811
|
+
db_path: c3.db のパス。省略時は locate_c3_db() で探索。
|
|
812
|
+
|
|
813
|
+
Returns:
|
|
814
|
+
upsert 成功時 True、DB 不在 / エラー時は静かに False。
|
|
815
|
+
"""
|
|
816
|
+
if db_path is None:
|
|
817
|
+
db_path = locate_c3_db()
|
|
818
|
+
if db_path is None:
|
|
819
|
+
return False
|
|
820
|
+
|
|
821
|
+
from datetime import timezone as _tz # noqa: PLC0415
|
|
822
|
+
now_iso = datetime.now(_tz.utc).isoformat(timespec="seconds")
|
|
823
|
+
|
|
824
|
+
try:
|
|
825
|
+
conn = sqlite3.connect(str(db_path))
|
|
826
|
+
try:
|
|
827
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
828
|
+
_apply_busy_timeout(conn)
|
|
829
|
+
conn.execute(
|
|
830
|
+
"INSERT INTO usage_ingest_state "
|
|
831
|
+
"(file_key, last_offset, last_processed_at) "
|
|
832
|
+
"VALUES (?, ?, ?) "
|
|
833
|
+
"ON CONFLICT(file_key) DO UPDATE SET "
|
|
834
|
+
" last_offset = excluded.last_offset, "
|
|
835
|
+
" last_processed_at = excluded.last_processed_at",
|
|
836
|
+
(file_key, offset, now_iso),
|
|
837
|
+
)
|
|
838
|
+
conn.commit()
|
|
839
|
+
finally:
|
|
840
|
+
conn.close()
|
|
841
|
+
return True
|
|
842
|
+
except Exception as exc: # noqa: BLE001
|
|
843
|
+
logger.warning("failed to set_ingest_offset: %s", type(exc).__name__)
|
|
844
|
+
return False
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- C3 SQLite migration 002: agent_cost_runs / usage_ingest_state テーブル追加
|
|
2
|
+
--
|
|
3
|
+
-- セッションログ(~/.claude/projects/<slug>/<session>.jsonl + subagents/)から
|
|
4
|
+
-- モデル別トークン消費を USD 換算して蓄積するコスト収集基盤テーブルを定義する。
|
|
5
|
+
--
|
|
6
|
+
-- PK=(session_id, agent_id, model) で「1 エージェント × 1 モデル = 1 行」。
|
|
7
|
+
-- jsonl 内の複数 requestId は ingester(usage_ingester.py)が合算 upsert する。
|
|
8
|
+
--
|
|
9
|
+
-- usage_ingest_state は jsonl ごとの処理済み行数 offset を管理する。
|
|
10
|
+
-- file_key = '<session>:mainline' / '<session>:agent-<id>'
|
|
11
|
+
|
|
12
|
+
BEGIN;
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS agent_cost_runs (
|
|
15
|
+
session_id TEXT NOT NULL,
|
|
16
|
+
agent_id TEXT NOT NULL, -- 'agent-<id>' / mainline は 'mainline'
|
|
17
|
+
agent_type TEXT NOT NULL, -- meta.json agentType / 'mainline'
|
|
18
|
+
description TEXT,
|
|
19
|
+
model TEXT NOT NULL, -- message.model
|
|
20
|
+
attribution_skill TEXT,
|
|
21
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
22
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
cache_create_tokens INTEGER NOT NULL DEFAULT 0,
|
|
25
|
+
total_cost_usd REAL NOT NULL,
|
|
26
|
+
recorded_at TEXT NOT NULL, -- ISO8601 UTC
|
|
27
|
+
PRIMARY KEY (session_id, agent_id, model)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_agent_cost_runs_agent_type
|
|
31
|
+
ON agent_cost_runs(agent_type, recorded_at);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS usage_ingest_state (
|
|
34
|
+
file_key TEXT PRIMARY KEY, -- '<session>:mainline' / '<session>:agent-<id>'
|
|
35
|
+
last_offset INTEGER NOT NULL, -- 処理済み行数
|
|
36
|
+
last_processed_at TEXT NOT NULL -- ISO8601 UTC
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
INSERT OR IGNORE INTO schema_migrations (version) VALUES ('002');
|
|
40
|
+
|
|
41
|
+
COMMIT;
|