shellbrain 0.1.22__tar.gz → 0.1.23__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.
Files changed (215) hide show
  1. {shellbrain-0.1.22 → shellbrain-0.1.23}/PKG-INFO +1 -1
  2. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/entities/telemetry.py +33 -0
  3. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/interfaces/repos.py +5 -0
  4. shellbrain-0.1.23/app/core/use_cases/record_model_usage_telemetry.py +20 -0
  5. shellbrain-0.1.23/app/migrations/versions/20260414_0010_model_usage_telemetry.py +83 -0
  6. shellbrain-0.1.23/app/migrations/versions/20260414_0011_usage_problem_tokens_multi_solution_metrics.py +147 -0
  7. shellbrain-0.1.23/app/migrations/versions/20260415_0012_read_pack_cost_and_read_roi.py +58 -0
  8. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/doctor.py +13 -0
  9. shellbrain-0.1.23/app/periphery/admin/model_usage_backfill.py +123 -0
  10. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/cli/handlers.py +23 -0
  11. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/cli/main.py +24 -0
  12. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/models/telemetry.py +53 -1
  13. shellbrain-0.1.23/app/periphery/db/models/views.py +662 -0
  14. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/relational/telemetry_repo.py +18 -0
  15. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/episodes/claude_code.py +63 -0
  16. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/episodes/codex.py +52 -0
  17. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/episodes/cursor.py +79 -0
  18. shellbrain-0.1.23/app/periphery/episodes/model_usage.py +226 -0
  19. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/episodes/poller.py +23 -0
  20. shellbrain-0.1.23/app/periphery/identity/cursor_statusline.py +238 -0
  21. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/onboarding/host_assets.py +128 -0
  22. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/telemetry/operation_summary.py +47 -0
  23. {shellbrain-0.1.22 → shellbrain-0.1.23}/pyproject.toml +1 -1
  24. {shellbrain-0.1.22 → shellbrain-0.1.23}/shellbrain.egg-info/PKG-INFO +1 -1
  25. {shellbrain-0.1.22 → shellbrain-0.1.23}/shellbrain.egg-info/SOURCES.txt +7 -0
  26. shellbrain-0.1.22/app/periphery/db/models/views.py +0 -154
  27. {shellbrain-0.1.22 → shellbrain-0.1.23}/README.md +0 -0
  28. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/__init__.py +0 -0
  29. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/__main__.py +0 -0
  30. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/__init__.py +0 -0
  31. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/_dsn_resolution.py +0 -0
  32. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/admin_db.py +0 -0
  33. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/config.py +0 -0
  34. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/create_policy.py +0 -0
  35. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/db.py +0 -0
  36. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/embeddings.py +0 -0
  37. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/home.py +0 -0
  38. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/migrations.py +0 -0
  39. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/read_policy.py +0 -0
  40. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/repos.py +0 -0
  41. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/retrieval.py +0 -0
  42. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/thresholds.py +0 -0
  43. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/update_policy.py +0 -0
  44. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/boot/use_cases.py +0 -0
  45. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/config/__init__.py +0 -0
  46. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/config/defaults/create_policy.yaml +0 -0
  47. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/config/defaults/read_policy.yaml +0 -0
  48. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/config/defaults/runtime.yaml +0 -0
  49. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/config/defaults/thresholds.yaml +0 -0
  50. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/config/defaults/update_policy.yaml +0 -0
  51. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/config/loader.py +0 -0
  52. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/__init__.py +0 -0
  53. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/contracts/__init__.py +0 -0
  54. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/contracts/errors.py +0 -0
  55. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/contracts/requests.py +0 -0
  56. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/contracts/responses.py +0 -0
  57. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/entities/__init__.py +0 -0
  58. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/entities/associations.py +0 -0
  59. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/entities/episodes.py +0 -0
  60. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/entities/evidence.py +0 -0
  61. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/entities/facts.py +0 -0
  62. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/entities/guidance.py +0 -0
  63. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/entities/identity.py +0 -0
  64. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/entities/memory.py +0 -0
  65. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/entities/runtime_context.py +0 -0
  66. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/entities/session_state.py +0 -0
  67. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/entities/utility.py +0 -0
  68. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/interfaces/__init__.py +0 -0
  69. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/interfaces/clock.py +0 -0
  70. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/interfaces/config.py +0 -0
  71. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/interfaces/embeddings.py +0 -0
  72. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/interfaces/idgen.py +0 -0
  73. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/interfaces/retrieval.py +0 -0
  74. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/interfaces/session_state_store.py +0 -0
  75. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/interfaces/unit_of_work.py +0 -0
  76. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/__init__.py +0 -0
  77. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/_shared/__init__.py +0 -0
  78. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/_shared/executor.py +0 -0
  79. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/_shared/side_effects.py +0 -0
  80. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/create_policy/__init__.py +0 -0
  81. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/create_policy/pipeline.py +0 -0
  82. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/read_policy/__init__.py +0 -0
  83. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/read_policy/bm25.py +0 -0
  84. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/read_policy/context_pack_builder.py +0 -0
  85. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/read_policy/expansion.py +0 -0
  86. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/read_policy/fusion_rrf.py +0 -0
  87. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/read_policy/lexical_query.py +0 -0
  88. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/read_policy/pipeline.py +0 -0
  89. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/read_policy/scoring.py +0 -0
  90. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/read_policy/seed_retrieval.py +0 -0
  91. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/update_policy/__init__.py +0 -0
  92. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/policies/update_policy/pipeline.py +0 -0
  93. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/use_cases/__init__.py +0 -0
  94. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/use_cases/build_guidance.py +0 -0
  95. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/use_cases/create_memory.py +0 -0
  96. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/use_cases/manage_session_state.py +0 -0
  97. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/use_cases/read_memory.py +0 -0
  98. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/use_cases/record_episode_sync_telemetry.py +0 -0
  99. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/use_cases/record_operation_telemetry.py +0 -0
  100. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/use_cases/sync_episode.py +0 -0
  101. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/core/use_cases/update_memory.py +0 -0
  102. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/migrations/__init__.py +0 -0
  103. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/migrations/env.py +0 -0
  104. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/migrations/versions/20260226_0001_initial_schema.py +0 -0
  105. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/migrations/versions/20260312_0002_add_hard_invariants.py +0 -0
  106. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/migrations/versions/20260312_0003_drop_create_confidence.py +0 -0
  107. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/migrations/versions/20260313_0004_episode_sync_hardening.py +0 -0
  108. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/migrations/versions/20260313_0005_evidence_episode_event_refs.py +0 -0
  109. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/migrations/versions/20260318_0006_usage_telemetry_schema.py +0 -0
  110. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/migrations/versions/20260319_0007_identity_session_guidance.py +0 -0
  111. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/migrations/versions/20260320_0008_instance_metadata_and_backup_safety.py +0 -0
  112. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/migrations/versions/20260410_0009_frontier_memory_family.py +0 -0
  113. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/migrations/versions/__init__.py +0 -0
  114. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/__init__.py +0 -0
  115. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/claude/CLAUDE.md +0 -0
  116. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/claude/skills/shellbrain-session-start/SKILL.md +0 -0
  117. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/claude/skills/shellbrain-usage-review/SKILL.md +0 -0
  118. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/codex/AGENTS.md +0 -0
  119. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/codex/shellbrain-session-start/SKILL.md +0 -0
  120. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/codex/shellbrain-session-start/agents/openai.yaml +0 -0
  121. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/codex/shellbrain-session-start/assets/shellbrain-large.svg +0 -0
  122. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/codex/shellbrain-session-start/assets/shellbrain-small.svg +0 -0
  123. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/codex/shellbrain-session-start/assets/shellbrain_logo.png +0 -0
  124. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/codex/shellbrain-session-start/references/request-shapes.md +0 -0
  125. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/codex/shellbrain-session-start/references/session-workflow.md +0 -0
  126. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/codex/shellbrain-usage-review/SKILL.md +0 -0
  127. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/codex/shellbrain-usage-review/agents/openai.yaml +0 -0
  128. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/codex/shellbrain-usage-review/assets/shellbrain-small.svg +0 -0
  129. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/codex/shellbrain-usage-review/assets/shellbrain_logo.png +0 -0
  130. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/cursor/skills/shellbrain-session-start/SKILL.md +0 -0
  131. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/onboarding_assets/cursor/skills/shellbrain-usage-review/SKILL.md +0 -0
  132. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/__init__.py +0 -0
  133. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/__init__.py +0 -0
  134. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/agent_behavior_analysis.py +0 -0
  135. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/analytics.py +0 -0
  136. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/analytics_diagnostics.py +0 -0
  137. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/analytics_queries.py +0 -0
  138. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/backup.py +0 -0
  139. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/destructive_guard.py +0 -0
  140. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/external_runtime.py +0 -0
  141. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/init.py +0 -0
  142. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/init_errors.py +0 -0
  143. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/instance_guard.py +0 -0
  144. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/machine_state.py +0 -0
  145. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/managed_runtime.py +0 -0
  146. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/privileges.py +0 -0
  147. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/repo_state.py +0 -0
  148. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/restore.py +0 -0
  149. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/storage_setup.py +0 -0
  150. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/admin/upgrade.py +0 -0
  151. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/cli/__init__.py +0 -0
  152. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/cli/hydration.py +0 -0
  153. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/cli/presenter_json.py +0 -0
  154. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/cli/schema_validation.py +0 -0
  155. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/__init__.py +0 -0
  156. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/engine.py +0 -0
  157. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/models/__init__.py +0 -0
  158. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/models/associations.py +0 -0
  159. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/models/episodes.py +0 -0
  160. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/models/evidence.py +0 -0
  161. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/models/experiences.py +0 -0
  162. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/models/instance_metadata.py +0 -0
  163. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/models/memories.py +0 -0
  164. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/models/metadata.py +0 -0
  165. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/models/registry.py +0 -0
  166. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/models/utility.py +0 -0
  167. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/__init__.py +0 -0
  168. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/relational/__init__.py +0 -0
  169. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/relational/associations_repo.py +0 -0
  170. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/relational/episodes_repo.py +0 -0
  171. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/relational/evidence_repo.py +0 -0
  172. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/relational/experiences_repo.py +0 -0
  173. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/relational/memories_repo.py +0 -0
  174. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/relational/read_policy_repo.py +0 -0
  175. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/relational/utility_repo.py +0 -0
  176. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/semantic/__init__.py +0 -0
  177. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/semantic/keyword_retrieval_repo.py +0 -0
  178. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/repos/semantic/semantic_retrieval_repo.py +0 -0
  179. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/session.py +0 -0
  180. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/db/uow.py +0 -0
  181. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/embeddings/__init__.py +0 -0
  182. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/embeddings/local_provider.py +0 -0
  183. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/embeddings/query_vector_search.py +0 -0
  184. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/episodes/__init__.py +0 -0
  185. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/episodes/launcher.py +0 -0
  186. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/episodes/normalization.py +0 -0
  187. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/episodes/poller_lock.py +0 -0
  188. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/episodes/source_discovery.py +0 -0
  189. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/episodes/tool_filter.py +0 -0
  190. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/identity/__init__.py +0 -0
  191. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/identity/claude_hook_install.py +0 -0
  192. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/identity/claude_runtime.py +0 -0
  193. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/identity/codex_runtime.py +0 -0
  194. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/identity/compatibility.py +0 -0
  195. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/identity/resolver.py +0 -0
  196. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/metrics/__init__.py +0 -0
  197. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/metrics/artifacts.py +0 -0
  198. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/metrics/browser.py +0 -0
  199. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/metrics/queries.py +0 -0
  200. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/metrics/render_html.py +0 -0
  201. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/metrics/service.py +0 -0
  202. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/onboarding/__init__.py +0 -0
  203. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/session_state/__init__.py +0 -0
  204. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/session_state/file_store.py +0 -0
  205. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/telemetry/__init__.py +0 -0
  206. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/telemetry/session_selection.py +0 -0
  207. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/telemetry/sync_summary.py +0 -0
  208. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/validation/__init__.py +0 -0
  209. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/validation/integrity_validation.py +0 -0
  210. {shellbrain-0.1.22 → shellbrain-0.1.23}/app/periphery/validation/semantic_validation.py +0 -0
  211. {shellbrain-0.1.22 → shellbrain-0.1.23}/setup.cfg +0 -0
  212. {shellbrain-0.1.22 → shellbrain-0.1.23}/shellbrain.egg-info/dependency_links.txt +0 -0
  213. {shellbrain-0.1.22 → shellbrain-0.1.23}/shellbrain.egg-info/entry_points.txt +0 -0
  214. {shellbrain-0.1.22 → shellbrain-0.1.23}/shellbrain.egg-info/requires.txt +0 -0
  215. {shellbrain-0.1.22 → shellbrain-0.1.23}/shellbrain.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shellbrain
3
- Version: 0.1.22
3
+ Version: 0.1.23
4
4
  Summary: Repo-scoped Shellbrain CLI with explicit evidence-backed writes.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -66,6 +66,12 @@ class ReadSummaryRecord:
66
66
  implicit_related_count: int
67
67
  total_returned: int
68
68
  zero_results: bool
69
+ pack_char_count: int | None
70
+ pack_token_estimate: int | None
71
+ pack_token_estimate_method: str | None
72
+ direct_token_estimate: int | None
73
+ explicit_related_token_estimate: int | None
74
+ implicit_related_token_estimate: int | None
69
75
  created_at: datetime
70
76
 
71
77
 
@@ -150,3 +156,30 @@ class EpisodeSyncToolTypeRecord:
150
156
  sync_run_id: str
151
157
  tool_type: str
152
158
  event_count: int
159
+
160
+
161
+ @dataclass(frozen=True)
162
+ class ModelUsageRecord:
163
+ """One normalized host model-usage event tied to a repo session."""
164
+
165
+ id: str
166
+ repo_id: str
167
+ thread_id: str | None
168
+ episode_id: str | None
169
+ host_app: str
170
+ host_session_key: str
171
+ host_usage_key: str
172
+ source_kind: str
173
+ occurred_at: datetime
174
+ agent_role: str
175
+ provider: str | None
176
+ model_id: str | None
177
+ input_tokens: int | None
178
+ output_tokens: int | None
179
+ reasoning_output_tokens: int | None
180
+ cached_input_tokens_total: int | None
181
+ cache_read_input_tokens: int | None
182
+ cache_creation_input_tokens: int | None
183
+ capture_quality: str
184
+ raw_usage_json: dict[str, Any] = field(default_factory=dict)
185
+ created_at: datetime | None = None
@@ -12,6 +12,7 @@ from app.core.entities.memory import Memory
12
12
  from app.core.entities.telemetry import (
13
13
  EpisodeSyncRunRecord,
14
14
  EpisodeSyncToolTypeRecord,
15
+ ModelUsageRecord,
15
16
  OperationInvocationRecord,
16
17
  ReadResultItemRecord,
17
18
  ReadSummaryRecord,
@@ -275,6 +276,10 @@ class ITelemetryRepo(ABC):
275
276
  ) -> None:
276
277
  """This method appends one sync-run row and its per-tool aggregates."""
277
278
 
279
+ @abstractmethod
280
+ def insert_model_usage(self, records: Sequence[ModelUsageRecord]) -> None:
281
+ """This method appends normalized model-usage rows idempotently."""
282
+
278
283
  @abstractmethod
279
284
  def update_operation_polling(self, invocation_id: str, *, attempted: bool, started: bool) -> None:
280
285
  """This method patches the poller-start flags for one existing invocation row."""
@@ -0,0 +1,20 @@
1
+ """Thin orchestration for normalized model-usage telemetry writes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from app.core.entities.telemetry import ModelUsageRecord
8
+ from app.core.interfaces.unit_of_work import IUnitOfWork
9
+
10
+
11
+ def record_model_usage_telemetry(
12
+ *,
13
+ uow: IUnitOfWork,
14
+ records: Sequence[ModelUsageRecord],
15
+ ) -> None:
16
+ """Persist normalized model-usage rows without affecting callers."""
17
+
18
+ if not records:
19
+ return
20
+ uow.telemetry.insert_model_usage(tuple(records))
@@ -0,0 +1,83 @@
1
+ """Add model-usage telemetry storage and derived views."""
2
+
3
+ from alembic import op
4
+
5
+ from app.periphery.db.models.views import (
6
+ USAGE_COMMAND_DAILY_SQL,
7
+ USAGE_MEMORY_RETRIEVAL_SQL,
8
+ USAGE_PROBLEM_TOKENS_SQL,
9
+ USAGE_SESSION_PROTOCOL_SQL,
10
+ USAGE_SESSION_TOKENS_SQL,
11
+ USAGE_SYNC_HEALTH_SQL,
12
+ USAGE_TOKEN_CAPTURE_HEALTH_SQL,
13
+ USAGE_WRITE_EFFECTS_SQL,
14
+ )
15
+
16
+ revision = "20260414_0010"
17
+ down_revision = "20260410_0009"
18
+ branch_labels = None
19
+ depends_on = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ """Create model-usage telemetry storage and derived views."""
24
+
25
+ op.execute(
26
+ """
27
+ CREATE TABLE model_usage (
28
+ id TEXT PRIMARY KEY,
29
+ repo_id TEXT NOT NULL,
30
+ thread_id TEXT,
31
+ episode_id TEXT,
32
+ host_app TEXT NOT NULL,
33
+ host_session_key TEXT NOT NULL,
34
+ host_usage_key TEXT NOT NULL,
35
+ source_kind TEXT NOT NULL,
36
+ occurred_at TIMESTAMPTZ NOT NULL,
37
+ agent_role TEXT NOT NULL DEFAULT 'foreground',
38
+ provider TEXT,
39
+ model_id TEXT,
40
+ input_tokens BIGINT,
41
+ output_tokens BIGINT,
42
+ reasoning_output_tokens BIGINT,
43
+ cached_input_tokens_total BIGINT,
44
+ cache_read_input_tokens BIGINT,
45
+ cache_creation_input_tokens BIGINT,
46
+ capture_quality TEXT NOT NULL DEFAULT 'exact',
47
+ raw_usage_json JSONB NOT NULL DEFAULT '{}'::jsonb,
48
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
49
+ CONSTRAINT uq_model_usage_host_session_usage UNIQUE (host_app, host_session_key, host_usage_key)
50
+ );
51
+ CREATE INDEX idx_model_usage_repo_thread_occurred_at
52
+ ON model_usage(repo_id, thread_id, occurred_at);
53
+ CREATE INDEX idx_model_usage_repo_host_session_occurred_at
54
+ ON model_usage(repo_id, host_app, host_session_key, occurred_at);
55
+ """
56
+ )
57
+ op.execute(USAGE_COMMAND_DAILY_SQL)
58
+ op.execute(USAGE_MEMORY_RETRIEVAL_SQL)
59
+ op.execute(USAGE_WRITE_EFFECTS_SQL)
60
+ op.execute(USAGE_SYNC_HEALTH_SQL)
61
+ op.execute(USAGE_SESSION_PROTOCOL_SQL)
62
+ op.execute(USAGE_SESSION_TOKENS_SQL)
63
+ op.execute(USAGE_PROBLEM_TOKENS_SQL)
64
+ op.execute(USAGE_TOKEN_CAPTURE_HEALTH_SQL)
65
+
66
+
67
+ def downgrade() -> None:
68
+ """Drop model-usage telemetry views and table."""
69
+
70
+ op.execute(
71
+ """
72
+ DROP VIEW IF EXISTS usage_token_capture_health;
73
+ DROP VIEW IF EXISTS usage_problem_tokens;
74
+ DROP VIEW IF EXISTS usage_session_tokens;
75
+ DROP VIEW IF EXISTS usage_session_protocol;
76
+ DROP VIEW IF EXISTS usage_sync_health;
77
+ DROP VIEW IF EXISTS usage_write_effects;
78
+ DROP VIEW IF EXISTS usage_memory_retrieval;
79
+ DROP VIEW IF EXISTS usage_command_daily;
80
+
81
+ DROP TABLE IF EXISTS model_usage;
82
+ """
83
+ )
@@ -0,0 +1,147 @@
1
+ """Expand usage_problem_tokens with latest-solution metrics."""
2
+
3
+ from alembic import op
4
+
5
+ from app.periphery.db.models.views import USAGE_PROBLEM_TOKENS_SQL
6
+
7
+ revision = "20260414_0011"
8
+ down_revision = "20260414_0010"
9
+ branch_labels = None
10
+ depends_on = None
11
+
12
+
13
+ PREVIOUS_USAGE_PROBLEM_TOKENS_SQL = """
14
+ CREATE OR REPLACE VIEW usage_problem_tokens AS
15
+ WITH session_quality AS (
16
+ SELECT
17
+ repo_id,
18
+ host_app,
19
+ host_session_key,
20
+ BOOL_OR(capture_quality = 'exact') AS has_exact_rows,
21
+ BOOL_OR(
22
+ capture_quality = 'exact'
23
+ AND (
24
+ COALESCE(input_tokens, 0) > 0
25
+ OR COALESCE(output_tokens, 0) > 0
26
+ OR COALESCE(cached_input_tokens_total, 0) > 0
27
+ OR COALESCE(reasoning_output_tokens, 0) > 0
28
+ )
29
+ ) AS has_nonzero_exact_rows,
30
+ BOOL_OR(capture_quality = 'estimated') AS has_estimated_rows
31
+ FROM model_usage
32
+ GROUP BY repo_id, host_app, host_session_key
33
+ ),
34
+ preferred_rows AS (
35
+ SELECT mu.*
36
+ FROM model_usage mu
37
+ JOIN session_quality sq
38
+ ON sq.repo_id = mu.repo_id
39
+ AND sq.host_app = mu.host_app
40
+ AND sq.host_session_key = mu.host_session_key
41
+ WHERE (
42
+ sq.has_nonzero_exact_rows
43
+ AND mu.capture_quality = 'exact'
44
+ ) OR (
45
+ NOT sq.has_nonzero_exact_rows
46
+ AND mu.capture_quality = 'estimated'
47
+ ) OR (
48
+ NOT sq.has_nonzero_exact_rows
49
+ AND NOT sq.has_estimated_rows
50
+ AND sq.has_exact_rows
51
+ AND mu.capture_quality = 'exact'
52
+ )
53
+ ),
54
+ problem_threads AS (
55
+ SELECT
56
+ p.id AS problem_id,
57
+ p.repo_id,
58
+ p.created_at AS problem_created_at,
59
+ (
60
+ ARRAY_AGG(ep.thread_id ORDER BY ee.created_at ASC, ee.seq ASC)
61
+ FILTER (WHERE ep.thread_id IS NOT NULL)
62
+ )[1] AS thread_id
63
+ FROM memories p
64
+ JOIN memory_evidence me ON me.memory_id = p.id
65
+ JOIN evidence_refs er ON er.id = me.evidence_id
66
+ JOIN episode_events ee ON ee.id = COALESCE(er.episode_event_id, er.ref)
67
+ JOIN episodes ep ON ep.id = ee.episode_id
68
+ WHERE p.kind = 'problem'
69
+ GROUP BY p.id, p.repo_id, p.created_at
70
+ ),
71
+ first_solutions AS (
72
+ SELECT
73
+ pa.problem_id,
74
+ s.id AS solution_id,
75
+ s.created_at AS solution_created_at,
76
+ ROW_NUMBER() OVER (
77
+ PARTITION BY pa.problem_id
78
+ ORDER BY s.created_at ASC, s.id ASC
79
+ ) AS row_num
80
+ FROM problem_attempts pa
81
+ JOIN memories s ON s.id = pa.attempt_id
82
+ WHERE pa.role = 'solution'
83
+ AND s.kind = 'solution'
84
+ ),
85
+ problem_windows AS (
86
+ SELECT
87
+ pt.repo_id,
88
+ pt.problem_id,
89
+ pt.thread_id,
90
+ pt.problem_created_at,
91
+ fs.solution_id,
92
+ fs.solution_created_at
93
+ FROM problem_threads pt
94
+ JOIN first_solutions fs
95
+ ON fs.problem_id = pt.problem_id
96
+ AND fs.row_num = 1
97
+ WHERE pt.thread_id IS NOT NULL
98
+ )
99
+ SELECT
100
+ pw.repo_id,
101
+ pw.problem_id,
102
+ pw.solution_id,
103
+ pw.thread_id,
104
+ pw.problem_created_at,
105
+ pw.solution_created_at,
106
+ COUNT(pr.id)::INTEGER AS usage_row_count,
107
+ COALESCE(SUM(pr.input_tokens), 0)::BIGINT AS input_tokens,
108
+ COALESCE(SUM(pr.output_tokens), 0)::BIGINT AS output_tokens,
109
+ COALESCE(SUM(pr.reasoning_output_tokens), 0)::BIGINT AS reasoning_output_tokens,
110
+ COALESCE(SUM(pr.cached_input_tokens_total), 0)::BIGINT AS cached_input_tokens_total,
111
+ COALESCE(SUM(pr.cache_read_input_tokens), 0)::BIGINT AS cache_read_input_tokens,
112
+ COALESCE(SUM(pr.cache_creation_input_tokens), 0)::BIGINT AS cache_creation_input_tokens,
113
+ (
114
+ COALESCE(SUM(pr.input_tokens), 0)
115
+ + COALESCE(SUM(pr.output_tokens), 0)
116
+ )::BIGINT AS fresh_work_tokens,
117
+ (
118
+ COALESCE(SUM(pr.input_tokens), 0)
119
+ + COALESCE(SUM(pr.cached_input_tokens_total), 0)
120
+ + COALESCE(SUM(pr.output_tokens), 0)
121
+ )::BIGINT AS all_tokens_including_cache
122
+ FROM problem_windows pw
123
+ LEFT JOIN preferred_rows pr
124
+ ON pr.repo_id = pw.repo_id
125
+ AND pr.thread_id = pw.thread_id
126
+ AND pr.occurred_at >= pw.problem_created_at
127
+ AND pr.occurred_at <= pw.solution_created_at
128
+ GROUP BY
129
+ pw.repo_id,
130
+ pw.problem_id,
131
+ pw.solution_id,
132
+ pw.thread_id,
133
+ pw.problem_created_at,
134
+ pw.solution_created_at;
135
+ """
136
+
137
+
138
+ def upgrade() -> None:
139
+ """Replace usage_problem_tokens with first- and latest-solution metrics."""
140
+
141
+ op.execute(USAGE_PROBLEM_TOKENS_SQL)
142
+
143
+
144
+ def downgrade() -> None:
145
+ """Restore the prior usage_problem_tokens definition."""
146
+
147
+ op.execute(PREVIOUS_USAGE_PROBLEM_TOKENS_SQL)
@@ -0,0 +1,58 @@
1
+ """Add read-pack cost telemetry and read-before-solve ROI views."""
2
+
3
+ from alembic import op
4
+
5
+ from app.periphery.db.models.views import (
6
+ USAGE_PROBLEM_READ_ROI_SQL,
7
+ USAGE_READ_BEFORE_SOLVE_ROI_SQL,
8
+ )
9
+
10
+ revision = "20260415_0012"
11
+ down_revision = "20260414_0011"
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade() -> None:
17
+ """Add read-pack estimate columns, a read-focused partial index, and ROI views."""
18
+
19
+ op.execute(
20
+ """
21
+ ALTER TABLE read_invocation_summaries
22
+ ADD COLUMN pack_char_count INTEGER,
23
+ ADD COLUMN pack_token_estimate INTEGER,
24
+ ADD COLUMN pack_token_estimate_method TEXT,
25
+ ADD COLUMN direct_token_estimate INTEGER,
26
+ ADD COLUMN explicit_related_token_estimate INTEGER,
27
+ ADD COLUMN implicit_related_token_estimate INTEGER;
28
+
29
+ CREATE INDEX idx_operation_invocations_successful_read_thread_created_at
30
+ ON operation_invocations(repo_id, selected_thread_id, created_at)
31
+ WHERE command = 'read'
32
+ AND outcome = 'ok'
33
+ AND selected_thread_id IS NOT NULL;
34
+ """
35
+ )
36
+ op.execute(USAGE_PROBLEM_READ_ROI_SQL)
37
+ op.execute(USAGE_READ_BEFORE_SOLVE_ROI_SQL)
38
+
39
+
40
+ def downgrade() -> None:
41
+ """Drop read ROI views, partial index, and read-pack estimate columns."""
42
+
43
+ op.execute(
44
+ """
45
+ DROP VIEW IF EXISTS usage_read_before_solve_roi;
46
+ DROP VIEW IF EXISTS usage_problem_read_roi;
47
+
48
+ DROP INDEX IF EXISTS idx_operation_invocations_successful_read_thread_created_at;
49
+
50
+ ALTER TABLE read_invocation_summaries
51
+ DROP COLUMN IF EXISTS implicit_related_token_estimate,
52
+ DROP COLUMN IF EXISTS explicit_related_token_estimate,
53
+ DROP COLUMN IF EXISTS direct_token_estimate,
54
+ DROP COLUMN IF EXISTS pack_token_estimate_method,
55
+ DROP COLUMN IF EXISTS pack_token_estimate,
56
+ DROP COLUMN IF EXISTS pack_char_count;
57
+ """
58
+ )
@@ -54,6 +54,18 @@ def build_doctor_report(
54
54
  disk = shutil.disk_usage(home_root if home_root.exists() else home_root.parent)
55
55
  repo_report = _build_repo_report(repo_root=repo_root)
56
56
  host_integrations = inspect_host_assets()
57
+ cursor_statusline = getattr(
58
+ host_integrations,
59
+ "cursor_statusline",
60
+ {
61
+ "installed": False,
62
+ "managed": False,
63
+ "malformed": False,
64
+ "path": None,
65
+ "command_executable": None,
66
+ "executable_exists": None,
67
+ },
68
+ )
57
69
  runtime_warnings = _runtime_warnings(machine_config)
58
70
 
59
71
  report: dict[str, Any] = {
@@ -88,6 +100,7 @@ def build_doctor_report(
88
100
  "claude_startup_guidance": host_integrations.claude_startup_guidance,
89
101
  "claude_skill": host_integrations.claude_skill,
90
102
  "cursor_skill": host_integrations.cursor_skill,
103
+ "cursor_statusline": cursor_statusline,
91
104
  "claude_global_hook": host_integrations.claude_global_hook,
92
105
  },
93
106
  "repo": repo_report,
@@ -0,0 +1,123 @@
1
+ """Retroactive token-usage backfill from Shellbrain-linked host transcripts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from sqlalchemy import text
10
+ from sqlalchemy.engine import Engine
11
+
12
+ from app.boot.use_cases import get_uow_factory
13
+ from app.core.entities.telemetry import ModelUsageRecord
14
+ from app.core.use_cases.record_model_usage_telemetry import record_model_usage_telemetry
15
+ from app.periphery.episodes.model_usage import collect_model_usage_records_for_session
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class BackfillSummary:
20
+ """Small structured summary for token-usage backfill runs."""
21
+
22
+ sessions_examined: int
23
+ sessions_with_records: int
24
+ sessions_skipped: int
25
+ sessions_failed: int
26
+ records_attempted: int
27
+ host_counts: dict[str, int]
28
+ errors: list[dict[str, str]]
29
+
30
+ def to_payload(self) -> dict[str, object]:
31
+ """Render the summary into JSON-safe primitives."""
32
+
33
+ return {
34
+ "sessions_examined": self.sessions_examined,
35
+ "sessions_with_records": self.sessions_with_records,
36
+ "sessions_skipped": self.sessions_skipped,
37
+ "sessions_failed": self.sessions_failed,
38
+ "records_attempted": self.records_attempted,
39
+ "host_counts": self.host_counts,
40
+ "errors": self.errors,
41
+ }
42
+
43
+
44
+ def backfill_model_usage(*, engine: Engine) -> BackfillSummary:
45
+ """Backfill normalized model usage for all Shellbrain-linked historical sessions."""
46
+
47
+ rows = _load_linked_sessions(engine=engine)
48
+ host_counts: Counter[str] = Counter()
49
+ errors: list[dict[str, str]] = []
50
+ sessions_with_records = 0
51
+ sessions_skipped = 0
52
+ sessions_failed = 0
53
+ records_attempted = 0
54
+ uow_factory = get_uow_factory()
55
+
56
+ for row in rows:
57
+ transcript_path = Path(str(row["transcript_path"]))
58
+ try:
59
+ records = collect_model_usage_records_for_session(
60
+ repo_id=str(row["repo_id"]),
61
+ host_app=str(row["host_app"]),
62
+ host_session_key=str(row["host_session_key"]),
63
+ thread_id=str(row["thread_id"]) if row["thread_id"] is not None else None,
64
+ episode_id=str(row["episode_id"]) if row["episode_id"] is not None else None,
65
+ transcript_path=transcript_path,
66
+ )
67
+ except Exception as exc:
68
+ sessions_failed += 1
69
+ errors.append(
70
+ {
71
+ "host_app": str(row["host_app"]),
72
+ "host_session_key": str(row["host_session_key"]),
73
+ "message": str(exc),
74
+ }
75
+ )
76
+ continue
77
+
78
+ if not records:
79
+ sessions_skipped += 1
80
+ continue
81
+ _persist_records(uow_factory=uow_factory, records=records)
82
+ sessions_with_records += 1
83
+ records_attempted += len(records)
84
+ host_counts[str(row["host_app"])] += len(records)
85
+
86
+ return BackfillSummary(
87
+ sessions_examined=len(rows),
88
+ sessions_with_records=sessions_with_records,
89
+ sessions_skipped=sessions_skipped,
90
+ sessions_failed=sessions_failed,
91
+ records_attempted=records_attempted,
92
+ host_counts=dict(sorted(host_counts.items())),
93
+ errors=errors,
94
+ )
95
+
96
+
97
+ def _persist_records(*, uow_factory, records: list[ModelUsageRecord]) -> None:
98
+ """Persist a batch of model-usage rows in one transaction."""
99
+
100
+ with uow_factory() as uow:
101
+ record_model_usage_telemetry(uow=uow, records=tuple(records))
102
+
103
+
104
+ def _load_linked_sessions(*, engine: Engine) -> list[dict[str, object]]:
105
+ """Return the latest Shellbrain-linked sync record per repo/host/session."""
106
+
107
+ statement = text(
108
+ """
109
+ SELECT DISTINCT ON (repo_id, host_app, host_session_key)
110
+ repo_id,
111
+ host_app,
112
+ host_session_key,
113
+ thread_id,
114
+ episode_id,
115
+ transcript_path
116
+ FROM episode_sync_runs
117
+ WHERE episode_id IS NOT NULL
118
+ AND transcript_path IS NOT NULL
119
+ ORDER BY repo_id, host_app, host_session_key, created_at DESC, id DESC
120
+ """
121
+ )
122
+ with engine.connect() as conn:
123
+ return [dict(row) for row in conn.execute(statement).mappings().all()]
@@ -26,6 +26,7 @@ from app.core.use_cases.manage_session_state import SessionStateManager
26
26
  from app.core.use_cases.create_memory import execute_create_memory
27
27
  from app.core.use_cases.read_memory import execute_read_memory
28
28
  from app.core.use_cases.record_episode_sync_telemetry import record_episode_sync_telemetry
29
+ from app.core.use_cases.record_model_usage_telemetry import record_model_usage_telemetry
29
30
  from app.core.use_cases.record_operation_telemetry import record_operation_telemetry
30
31
  from app.core.use_cases.sync_episode import sync_episode
31
32
  from app.core.use_cases.update_memory import execute_update_memory
@@ -46,6 +47,7 @@ from app.periphery.cli.schema_validation import (
46
47
  validate_update_schema,
47
48
  )
48
49
  from app.periphery.episodes.normalization import normalize_host_transcript
50
+ from app.periphery.episodes.model_usage import collect_model_usage_records_for_session
49
51
  from app.periphery.identity.resolver import (
50
52
  discover_untrusted_events_candidate,
51
53
  resolve_caller_identity,
@@ -305,6 +307,7 @@ def handle_events(
305
307
  selection_summary = SessionSelectionSummary()
306
308
  sync_run = None
307
309
  sync_tool_types = ()
310
+ model_usage_records = ()
308
311
  try:
309
312
  agent_request, errors = validate_events_schema(payload)
310
313
  if errors:
@@ -392,6 +395,19 @@ def handle_events(
392
395
  system_event_count=int(sync_result["system_event_count"]),
393
396
  tool_type_counts=dict(sync_result["tool_type_counts"]),
394
397
  )
398
+ try:
399
+ model_usage_records = tuple(
400
+ collect_model_usage_records_for_session(
401
+ repo_id=request.repo_id,
402
+ host_app=str(source.host_app),
403
+ host_session_key=str(source.host_session_key),
404
+ thread_id=str(sync_result["thread_id"]),
405
+ episode_id=str(sync_result["episode_id"]),
406
+ transcript_path=Path(str(sync_result["transcript_path"])),
407
+ )
408
+ )
409
+ except Exception:
410
+ model_usage_records = ()
395
411
  except Exception as exc:
396
412
  error_stage = "sync"
397
413
  result = _error_response([ErrorDetail(code=ErrorCode.INTERNAL_ERROR, message=str(exc))])
@@ -436,6 +452,7 @@ def handle_events(
436
452
  selection_summary=selection_summary,
437
453
  sync_run=sync_run,
438
454
  sync_tool_types=sync_tool_types,
455
+ model_usage_records=model_usage_records,
439
456
  total_latency_ms=int((perf_counter() - started_at) * 1000),
440
457
  )
441
458
  return result
@@ -653,6 +670,7 @@ def _persist_operation_telemetry_best_effort(
653
670
  selection_summary: SessionSelectionSummary | None = None,
654
671
  sync_run=None,
655
672
  sync_tool_types=(),
673
+ model_usage_records=(),
656
674
  total_latency_ms: int | None = None,
657
675
  ) -> None:
658
676
  """Persist invocation telemetry in a second best-effort transaction."""
@@ -716,6 +734,11 @@ def _persist_operation_telemetry_best_effort(
716
734
  run=sync_run,
717
735
  tool_types=sync_tool_types,
718
736
  )
737
+ if model_usage_records:
738
+ record_model_usage_telemetry(
739
+ uow=telemetry_uow,
740
+ records=tuple(model_usage_records),
741
+ )
719
742
  except Exception:
720
743
  return
721
744
 
@@ -230,6 +230,15 @@ _ANALYTICS_HELP = dedent(
230
230
  """
231
231
  )
232
232
 
233
+ _BACKFILL_TOKEN_USAGE_HELP = dedent(
234
+ """\
235
+ Backfill normalized model-token telemetry from Shellbrain-linked host session files.
236
+
237
+ Example:
238
+ shellbrain admin backfill-token-usage
239
+ """
240
+ )
241
+
233
242
  _METRICS_HELP = dedent(
234
243
  """\
235
244
  Generate one lightweight repo-scoped metrics snapshot, write local artifacts, and open a static dashboard.
@@ -470,6 +479,13 @@ def build_parser() -> argparse.ArgumentParser:
470
479
  default=2,
471
480
  help="Number of trailing days to include in the report. Defaults to 2.",
472
481
  )
482
+ admin_subparsers.add_parser(
483
+ "backfill-token-usage",
484
+ help="Backfill normalized token usage from linked host session files.",
485
+ description="Backfill normalized token usage from Shellbrain-linked host session files.",
486
+ epilog=_BACKFILL_TOKEN_USAGE_HELP,
487
+ formatter_class=_HelpFormatter,
488
+ )
473
489
  install_hook_parser = admin_subparsers.add_parser(
474
490
  "install-claude-hook",
475
491
  help="Install the repo-local Claude hook used for trusted caller identity.",
@@ -771,6 +787,14 @@ def _run_admin_command(args: argparse.Namespace) -> int:
771
787
  print(json.dumps(report, indent=2, sort_keys=True))
772
788
  return 0
773
789
 
790
+ if args.admin_command == "backfill-token-usage":
791
+ from app.boot.db import get_engine_instance
792
+ from app.periphery.admin.model_usage_backfill import backfill_model_usage
793
+
794
+ summary = backfill_model_usage(engine=get_engine_instance())
795
+ print(json.dumps(summary.to_payload(), indent=2, sort_keys=True))
796
+ return 0
797
+
774
798
  repo_root = _resolve_admin_repo_root(getattr(args, "repo_root", None))
775
799
  if args.admin_command == "install-claude-hook":
776
800
  from app.periphery.identity.claude_hook_install import install_claude_hook