shellbrain 0.1.22__tar.gz → 0.1.24__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 (216) hide show
  1. {shellbrain-0.1.22 → shellbrain-0.1.24}/PKG-INFO +1 -1
  2. shellbrain-0.1.24/app/boot/migrations.py +115 -0
  3. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/entities/telemetry.py +33 -0
  4. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/interfaces/repos.py +5 -0
  5. shellbrain-0.1.24/app/core/use_cases/record_model_usage_telemetry.py +20 -0
  6. shellbrain-0.1.24/app/migrations/versions/20260414_0010_model_usage_telemetry.py +83 -0
  7. shellbrain-0.1.24/app/migrations/versions/20260414_0011_usage_problem_tokens_multi_solution_metrics.py +147 -0
  8. shellbrain-0.1.24/app/migrations/versions/20260415_0012_read_pack_cost_and_read_roi.py +58 -0
  9. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/doctor.py +13 -0
  10. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/init.py +5 -2
  11. shellbrain-0.1.24/app/periphery/admin/model_usage_backfill.py +123 -0
  12. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/cli/handlers.py +23 -0
  13. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/cli/main.py +30 -2
  14. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/models/telemetry.py +53 -1
  15. shellbrain-0.1.24/app/periphery/db/models/views.py +662 -0
  16. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/relational/telemetry_repo.py +18 -0
  17. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/episodes/claude_code.py +63 -0
  18. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/episodes/codex.py +52 -0
  19. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/episodes/cursor.py +79 -0
  20. shellbrain-0.1.24/app/periphery/episodes/model_usage.py +226 -0
  21. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/episodes/poller.py +23 -0
  22. shellbrain-0.1.24/app/periphery/identity/cursor_statusline.py +238 -0
  23. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/onboarding/host_assets.py +128 -0
  24. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/telemetry/operation_summary.py +47 -0
  25. {shellbrain-0.1.22 → shellbrain-0.1.24}/pyproject.toml +1 -1
  26. {shellbrain-0.1.22 → shellbrain-0.1.24}/shellbrain.egg-info/PKG-INFO +1 -1
  27. {shellbrain-0.1.22 → shellbrain-0.1.24}/shellbrain.egg-info/SOURCES.txt +7 -0
  28. shellbrain-0.1.22/app/boot/migrations.py +0 -61
  29. shellbrain-0.1.22/app/periphery/db/models/views.py +0 -154
  30. {shellbrain-0.1.22 → shellbrain-0.1.24}/README.md +0 -0
  31. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/__init__.py +0 -0
  32. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/__main__.py +0 -0
  33. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/__init__.py +0 -0
  34. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/_dsn_resolution.py +0 -0
  35. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/admin_db.py +0 -0
  36. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/config.py +0 -0
  37. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/create_policy.py +0 -0
  38. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/db.py +0 -0
  39. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/embeddings.py +0 -0
  40. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/home.py +0 -0
  41. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/read_policy.py +0 -0
  42. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/repos.py +0 -0
  43. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/retrieval.py +0 -0
  44. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/thresholds.py +0 -0
  45. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/update_policy.py +0 -0
  46. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/boot/use_cases.py +0 -0
  47. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/config/__init__.py +0 -0
  48. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/config/defaults/create_policy.yaml +0 -0
  49. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/config/defaults/read_policy.yaml +0 -0
  50. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/config/defaults/runtime.yaml +0 -0
  51. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/config/defaults/thresholds.yaml +0 -0
  52. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/config/defaults/update_policy.yaml +0 -0
  53. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/config/loader.py +0 -0
  54. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/__init__.py +0 -0
  55. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/contracts/__init__.py +0 -0
  56. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/contracts/errors.py +0 -0
  57. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/contracts/requests.py +0 -0
  58. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/contracts/responses.py +0 -0
  59. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/entities/__init__.py +0 -0
  60. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/entities/associations.py +0 -0
  61. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/entities/episodes.py +0 -0
  62. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/entities/evidence.py +0 -0
  63. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/entities/facts.py +0 -0
  64. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/entities/guidance.py +0 -0
  65. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/entities/identity.py +0 -0
  66. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/entities/memory.py +0 -0
  67. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/entities/runtime_context.py +0 -0
  68. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/entities/session_state.py +0 -0
  69. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/entities/utility.py +0 -0
  70. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/interfaces/__init__.py +0 -0
  71. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/interfaces/clock.py +0 -0
  72. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/interfaces/config.py +0 -0
  73. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/interfaces/embeddings.py +0 -0
  74. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/interfaces/idgen.py +0 -0
  75. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/interfaces/retrieval.py +0 -0
  76. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/interfaces/session_state_store.py +0 -0
  77. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/interfaces/unit_of_work.py +0 -0
  78. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/__init__.py +0 -0
  79. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/_shared/__init__.py +0 -0
  80. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/_shared/executor.py +0 -0
  81. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/_shared/side_effects.py +0 -0
  82. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/create_policy/__init__.py +0 -0
  83. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/create_policy/pipeline.py +0 -0
  84. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/read_policy/__init__.py +0 -0
  85. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/read_policy/bm25.py +0 -0
  86. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/read_policy/context_pack_builder.py +0 -0
  87. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/read_policy/expansion.py +0 -0
  88. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/read_policy/fusion_rrf.py +0 -0
  89. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/read_policy/lexical_query.py +0 -0
  90. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/read_policy/pipeline.py +0 -0
  91. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/read_policy/scoring.py +0 -0
  92. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/read_policy/seed_retrieval.py +0 -0
  93. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/update_policy/__init__.py +0 -0
  94. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/policies/update_policy/pipeline.py +0 -0
  95. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/use_cases/__init__.py +0 -0
  96. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/use_cases/build_guidance.py +0 -0
  97. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/use_cases/create_memory.py +0 -0
  98. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/use_cases/manage_session_state.py +0 -0
  99. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/use_cases/read_memory.py +0 -0
  100. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/use_cases/record_episode_sync_telemetry.py +0 -0
  101. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/use_cases/record_operation_telemetry.py +0 -0
  102. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/use_cases/sync_episode.py +0 -0
  103. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/core/use_cases/update_memory.py +0 -0
  104. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/migrations/__init__.py +0 -0
  105. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/migrations/env.py +0 -0
  106. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/migrations/versions/20260226_0001_initial_schema.py +0 -0
  107. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/migrations/versions/20260312_0002_add_hard_invariants.py +0 -0
  108. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/migrations/versions/20260312_0003_drop_create_confidence.py +0 -0
  109. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/migrations/versions/20260313_0004_episode_sync_hardening.py +0 -0
  110. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/migrations/versions/20260313_0005_evidence_episode_event_refs.py +0 -0
  111. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/migrations/versions/20260318_0006_usage_telemetry_schema.py +0 -0
  112. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/migrations/versions/20260319_0007_identity_session_guidance.py +0 -0
  113. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/migrations/versions/20260320_0008_instance_metadata_and_backup_safety.py +0 -0
  114. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/migrations/versions/20260410_0009_frontier_memory_family.py +0 -0
  115. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/migrations/versions/__init__.py +0 -0
  116. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/__init__.py +0 -0
  117. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/claude/CLAUDE.md +0 -0
  118. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/claude/skills/shellbrain-session-start/SKILL.md +0 -0
  119. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/claude/skills/shellbrain-usage-review/SKILL.md +0 -0
  120. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/codex/AGENTS.md +0 -0
  121. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/codex/shellbrain-session-start/SKILL.md +0 -0
  122. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/codex/shellbrain-session-start/agents/openai.yaml +0 -0
  123. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/codex/shellbrain-session-start/assets/shellbrain-large.svg +0 -0
  124. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/codex/shellbrain-session-start/assets/shellbrain-small.svg +0 -0
  125. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/codex/shellbrain-session-start/assets/shellbrain_logo.png +0 -0
  126. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/codex/shellbrain-session-start/references/request-shapes.md +0 -0
  127. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/codex/shellbrain-session-start/references/session-workflow.md +0 -0
  128. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/codex/shellbrain-usage-review/SKILL.md +0 -0
  129. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/codex/shellbrain-usage-review/agents/openai.yaml +0 -0
  130. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/codex/shellbrain-usage-review/assets/shellbrain-small.svg +0 -0
  131. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/codex/shellbrain-usage-review/assets/shellbrain_logo.png +0 -0
  132. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/cursor/skills/shellbrain-session-start/SKILL.md +0 -0
  133. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/onboarding_assets/cursor/skills/shellbrain-usage-review/SKILL.md +0 -0
  134. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/__init__.py +0 -0
  135. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/__init__.py +0 -0
  136. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/agent_behavior_analysis.py +0 -0
  137. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/analytics.py +0 -0
  138. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/analytics_diagnostics.py +0 -0
  139. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/analytics_queries.py +0 -0
  140. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/backup.py +0 -0
  141. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/destructive_guard.py +0 -0
  142. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/external_runtime.py +0 -0
  143. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/init_errors.py +0 -0
  144. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/instance_guard.py +0 -0
  145. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/machine_state.py +0 -0
  146. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/managed_runtime.py +0 -0
  147. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/privileges.py +0 -0
  148. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/repo_state.py +0 -0
  149. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/restore.py +0 -0
  150. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/storage_setup.py +0 -0
  151. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/admin/upgrade.py +0 -0
  152. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/cli/__init__.py +0 -0
  153. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/cli/hydration.py +0 -0
  154. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/cli/presenter_json.py +0 -0
  155. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/cli/schema_validation.py +0 -0
  156. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/__init__.py +0 -0
  157. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/engine.py +0 -0
  158. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/models/__init__.py +0 -0
  159. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/models/associations.py +0 -0
  160. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/models/episodes.py +0 -0
  161. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/models/evidence.py +0 -0
  162. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/models/experiences.py +0 -0
  163. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/models/instance_metadata.py +0 -0
  164. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/models/memories.py +0 -0
  165. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/models/metadata.py +0 -0
  166. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/models/registry.py +0 -0
  167. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/models/utility.py +0 -0
  168. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/__init__.py +0 -0
  169. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/relational/__init__.py +0 -0
  170. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/relational/associations_repo.py +0 -0
  171. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/relational/episodes_repo.py +0 -0
  172. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/relational/evidence_repo.py +0 -0
  173. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/relational/experiences_repo.py +0 -0
  174. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/relational/memories_repo.py +0 -0
  175. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/relational/read_policy_repo.py +0 -0
  176. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/relational/utility_repo.py +0 -0
  177. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/semantic/__init__.py +0 -0
  178. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/semantic/keyword_retrieval_repo.py +0 -0
  179. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/repos/semantic/semantic_retrieval_repo.py +0 -0
  180. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/session.py +0 -0
  181. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/db/uow.py +0 -0
  182. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/embeddings/__init__.py +0 -0
  183. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/embeddings/local_provider.py +0 -0
  184. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/embeddings/query_vector_search.py +0 -0
  185. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/episodes/__init__.py +0 -0
  186. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/episodes/launcher.py +0 -0
  187. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/episodes/normalization.py +0 -0
  188. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/episodes/poller_lock.py +0 -0
  189. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/episodes/source_discovery.py +0 -0
  190. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/episodes/tool_filter.py +0 -0
  191. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/identity/__init__.py +0 -0
  192. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/identity/claude_hook_install.py +0 -0
  193. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/identity/claude_runtime.py +0 -0
  194. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/identity/codex_runtime.py +0 -0
  195. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/identity/compatibility.py +0 -0
  196. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/identity/resolver.py +0 -0
  197. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/metrics/__init__.py +0 -0
  198. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/metrics/artifacts.py +0 -0
  199. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/metrics/browser.py +0 -0
  200. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/metrics/queries.py +0 -0
  201. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/metrics/render_html.py +0 -0
  202. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/metrics/service.py +0 -0
  203. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/onboarding/__init__.py +0 -0
  204. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/session_state/__init__.py +0 -0
  205. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/session_state/file_store.py +0 -0
  206. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/telemetry/__init__.py +0 -0
  207. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/telemetry/session_selection.py +0 -0
  208. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/telemetry/sync_summary.py +0 -0
  209. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/validation/__init__.py +0 -0
  210. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/validation/integrity_validation.py +0 -0
  211. {shellbrain-0.1.22 → shellbrain-0.1.24}/app/periphery/validation/semantic_validation.py +0 -0
  212. {shellbrain-0.1.22 → shellbrain-0.1.24}/setup.cfg +0 -0
  213. {shellbrain-0.1.22 → shellbrain-0.1.24}/shellbrain.egg-info/dependency_links.txt +0 -0
  214. {shellbrain-0.1.22 → shellbrain-0.1.24}/shellbrain.egg-info/entry_points.txt +0 -0
  215. {shellbrain-0.1.22 → shellbrain-0.1.24}/shellbrain.egg-info/requires.txt +0 -0
  216. {shellbrain-0.1.22 → shellbrain-0.1.24}/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.24
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
@@ -0,0 +1,115 @@
1
+ """Packaged Alembic bootstrap helpers for installed-shellbrain database migrations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.resources import as_file, files
6
+ import importlib.metadata
7
+
8
+ from alembic import command
9
+ from alembic.config import Config
10
+ from alembic.script import ScriptDirectory
11
+ from alembic.script.revision import ResolutionError
12
+
13
+ from app.boot.admin_db import get_admin_db_dsn, get_backup_dir, get_backup_mirror_dir, get_instance_mode_default
14
+ from app.boot.db import get_optional_db_dsn
15
+ from app.periphery.admin.destructive_guard import backup_and_verify_before_destructive_action
16
+ from app.periphery.admin.instance_guard import ensure_instance_metadata, fetch_instance_metadata
17
+ from app.periphery.admin.privileges import reconcile_app_role_privileges
18
+
19
+
20
+ class DatabaseRevisionAheadOfInstalledPackageError(RuntimeError):
21
+ """Raised when the target database revision is newer than the installed package knows about."""
22
+
23
+
24
+ def upgrade_database(revision: str = "head") -> None:
25
+ """Apply packaged Alembic migrations to the configured database."""
26
+
27
+ config = Config()
28
+ admin_dsn = get_admin_db_dsn()
29
+ with as_file(files("app").joinpath("migrations")) as migrations_path:
30
+ config.set_main_option("script_location", str(migrations_path))
31
+ script = ScriptDirectory.from_config(config)
32
+ if _database_has_shellbrain_objects(admin_dsn):
33
+ _assert_database_revision_is_known(admin_dsn=admin_dsn, script=script)
34
+ backup_and_verify_before_destructive_action(
35
+ admin_dsn=admin_dsn,
36
+ backup_root=get_backup_dir(),
37
+ mirror_root=get_backup_mirror_dir(),
38
+ )
39
+ config.set_main_option("sqlalchemy.url", admin_dsn)
40
+ command.upgrade(config, revision)
41
+ if fetch_instance_metadata(admin_dsn) is None:
42
+ ensure_instance_metadata(
43
+ admin_dsn,
44
+ instance_mode=get_instance_mode_default(),
45
+ created_by="app.admin.migrate",
46
+ notes="Stamped by packaged migration runner.",
47
+ )
48
+ app_dsn = get_optional_db_dsn()
49
+ if app_dsn:
50
+ reconcile_app_role_privileges(admin_dsn=admin_dsn, app_dsn=app_dsn)
51
+
52
+
53
+ def _database_has_shellbrain_objects(admin_dsn: str) -> bool:
54
+ """Return whether the target database already contains Shellbrain-managed tables."""
55
+
56
+ import psycopg
57
+
58
+ with psycopg.connect(admin_dsn.replace("+psycopg", "")) as conn:
59
+ with conn.cursor() as cur:
60
+ cur.execute(
61
+ """
62
+ SELECT EXISTS (
63
+ SELECT 1
64
+ FROM information_schema.tables
65
+ WHERE table_schema = 'public'
66
+ AND table_name IN ('memories', 'episodes', 'episode_events', 'operation_invocations')
67
+ )
68
+ """
69
+ )
70
+ return bool(cur.fetchone()[0])
71
+
72
+
73
+ def _assert_database_revision_is_known(*, admin_dsn: str, script: ScriptDirectory) -> None:
74
+ """Fail early with a user-facing error when the database revision is newer than this package."""
75
+
76
+ current_revision = _fetch_database_revision(admin_dsn)
77
+ if current_revision is None:
78
+ return
79
+ try:
80
+ script.get_revision(current_revision)
81
+ except ResolutionError as exc:
82
+ installed_version = _installed_shellbrain_version()
83
+ raise DatabaseRevisionAheadOfInstalledPackageError(
84
+ "Installed Shellbrain package "
85
+ f"({installed_version}) cannot manage database revision {current_revision}. "
86
+ "This database was likely migrated by a newer Shellbrain release than the one currently installed. "
87
+ "Upgrade Shellbrain to a build that includes this revision, then rerun `shellbrain init` or "
88
+ "`shellbrain admin migrate`."
89
+ ) from exc
90
+
91
+
92
+ def _fetch_database_revision(admin_dsn: str) -> str | None:
93
+ """Return the current alembic revision when present."""
94
+
95
+ import psycopg
96
+
97
+ try:
98
+ with psycopg.connect(admin_dsn.replace("+psycopg", "")) as conn:
99
+ with conn.cursor() as cur:
100
+ cur.execute("SELECT version_num FROM alembic_version")
101
+ row = cur.fetchone()
102
+ except psycopg.Error:
103
+ return None
104
+ if row is None or row[0] is None:
105
+ return None
106
+ return str(row[0])
107
+
108
+
109
+ def _installed_shellbrain_version() -> str:
110
+ """Return the installed Shellbrain package version when available."""
111
+
112
+ try:
113
+ return importlib.metadata.version("shellbrain")
114
+ except importlib.metadata.PackageNotFoundError:
115
+ return "dev"
@@ -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,
@@ -469,10 +469,13 @@ def _reconcile_database(config: MachineConfig) -> tuple[bool, MachineConfig]:
469
469
  def _apply_schema_migrations(config: MachineConfig) -> bool:
470
470
  """Apply packaged schema migrations to the configured Shellbrain database."""
471
471
 
472
- from app.boot.migrations import upgrade_database
472
+ from app.boot.migrations import DatabaseRevisionAheadOfInstalledPackageError, upgrade_database
473
473
 
474
474
  before_revision = _fetch_schema_revision(config.database.admin_dsn)
475
- upgrade_database()
475
+ try:
476
+ upgrade_database()
477
+ except DatabaseRevisionAheadOfInstalledPackageError as exc:
478
+ raise InitConflictError(str(exc)) from exc
476
479
  after_revision = _fetch_schema_revision(config.database.admin_dsn)
477
480
  return before_revision != after_revision
478
481
 
@@ -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()]