sidegraph 0.3.0__tar.gz → 0.3.1__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 (363) hide show
  1. {sidegraph-0.3.0 → sidegraph-0.3.1}/CHANGELOG.md +20 -0
  2. {sidegraph-0.3.0 → sidegraph-0.3.1}/CLAUDE.md +1 -1
  3. {sidegraph-0.3.0 → sidegraph-0.3.1}/PKG-INFO +1 -1
  4. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/hooks.md +30 -9
  5. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/releasing.md +16 -0
  6. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/.claude-plugin/plugin.json +1 -1
  7. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/.codex-plugin/plugin.json +1 -1
  8. {sidegraph-0.3.0 → sidegraph-0.3.1}/pyproject.toml +1 -1
  9. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/__init__.py +1 -1
  10. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/config.py +8 -0
  11. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/host/hooks.py +48 -12
  12. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_drift_nudge.py +8 -5
  13. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_pretool.py +33 -0
  14. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_session_key.py +93 -0
  15. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_stop.py +31 -2
  16. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_touch_events.py +25 -0
  17. {sidegraph-0.3.0 → sidegraph-0.3.1}/uv.lock +1 -1
  18. {sidegraph-0.3.0 → sidegraph-0.3.1}/.agents/plugins/marketplace.json +0 -0
  19. {sidegraph-0.3.0 → sidegraph-0.3.1}/.claude-plugin/marketplace.json +0 -0
  20. {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/CODEOWNERS +0 -0
  21. {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  22. {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  23. {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  24. {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  25. {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/secret_scanning.yml +0 -0
  26. {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/workflows/ci.yml +0 -0
  27. {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/workflows/publish.yml +0 -0
  28. {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/workflows/scorecard.yml +0 -0
  29. {sidegraph-0.3.0 → sidegraph-0.3.1}/.gitignore +0 -0
  30. {sidegraph-0.3.0 → sidegraph-0.3.1}/.gitleaks.toml +0 -0
  31. {sidegraph-0.3.0 → sidegraph-0.3.1}/.pre-commit-config.yaml +0 -0
  32. {sidegraph-0.3.0 → sidegraph-0.3.1}/.python-version +0 -0
  33. {sidegraph-0.3.0 → sidegraph-0.3.1}/AGENTS.md +0 -0
  34. {sidegraph-0.3.0 → sidegraph-0.3.1}/CODE_OF_CONDUCT.md +0 -0
  35. {sidegraph-0.3.0 → sidegraph-0.3.1}/CONTRIBUTING.md +0 -0
  36. {sidegraph-0.3.0 → sidegraph-0.3.1}/LICENSE +0 -0
  37. {sidegraph-0.3.0 → sidegraph-0.3.1}/README.md +0 -0
  38. {sidegraph-0.3.0 → sidegraph-0.3.1}/SECURITY.md +0 -0
  39. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/README.md +0 -0
  40. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/concepts/anchoring.md +0 -0
  41. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/concepts/data-model.md +0 -0
  42. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/concepts/decision-memory.md +0 -0
  43. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/concepts/mind-model.md +0 -0
  44. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/concepts/retrieval.md +0 -0
  45. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/getting-started/bootstrap.md +0 -0
  46. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/getting-started/claude-code-setup.md +0 -0
  47. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/getting-started/codex-setup.md +0 -0
  48. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/getting-started/installation.md +0 -0
  49. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/getting-started/quickstart.md +0 -0
  50. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/capturing-decisions.md +0 -0
  51. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/ci-cd-maintenance.md +0 -0
  52. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/naming-your-domains.md +0 -0
  53. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/retrieval-in-sessions.md +0 -0
  54. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/semantic-docs.md +0 -0
  55. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/surviving-refactors.md +0 -0
  56. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/team-workflow.md +0 -0
  57. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/verifying-your-setup.md +0 -0
  58. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/integrations/claude-code.md +0 -0
  59. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/integrations/codex.md +0 -0
  60. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/integrations/graphify.md +0 -0
  61. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/llms.txt +0 -0
  62. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/pilot-kit/README.md +0 -0
  63. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/pilot-kit/corpus_fit.py +0 -0
  64. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/pilot-kit/judge-prompt.md +0 -0
  65. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/pilot-kit/questions-prompt.md +0 -0
  66. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/pilot-kit/rubric-template.md +0 -0
  67. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/cli.md +0 -0
  68. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/configuration.md +0 -0
  69. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/git-bindings.md +0 -0
  70. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/mcp-tools.md +0 -0
  71. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/operations.md +0 -0
  72. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/stability.md +0 -0
  73. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/store-format.md +0 -0
  74. {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/whitepaper/index.md +0 -0
  75. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/.mcp.json +0 -0
  76. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/codex/hooks.json +0 -0
  77. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/codex/mcp.json +0 -0
  78. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/hooks/hooks.json +0 -0
  79. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/check-plan/SKILL.md +0 -0
  80. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/check-plan/agents/openai.yaml +0 -0
  81. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/explain-why/SKILL.md +0 -0
  82. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/explain-why/agents/openai.yaml +0 -0
  83. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/heal-anchors/SKILL.md +0 -0
  84. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/heal-anchors/agents/openai.yaml +0 -0
  85. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/import-adrs/SKILL.md +0 -0
  86. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/import-adrs/agents/openai.yaml +0 -0
  87. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/manage-domains/SKILL.md +0 -0
  88. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/manage-domains/agents/openai.yaml +0 -0
  89. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/name-domains/SKILL.md +0 -0
  90. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/name-domains/agents/openai.yaml +0 -0
  91. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/ratify-decisions/SKILL.md +0 -0
  92. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/ratify-decisions/agents/openai.yaml +0 -0
  93. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/record-decision/SKILL.md +0 -0
  94. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/record-decision/agents/openai.yaml +0 -0
  95. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/record-fact/SKILL.md +0 -0
  96. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/record-fact/agents/openai.yaml +0 -0
  97. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/setup/SKILL.md +0 -0
  98. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/setup/agents/openai.yaml +0 -0
  99. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/stats/SKILL.md +0 -0
  100. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/stats/agents/openai.yaml +0 -0
  101. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/triage-drift/SKILL.md +0 -0
  102. {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/triage-drift/agents/openai.yaml +0 -0
  103. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/anchoring.py +0 -0
  104. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/__init__.py +0 -0
  105. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/apply.py +0 -0
  106. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/catalog.py +0 -0
  107. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/cli.py +0 -0
  108. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/integrations.py +0 -0
  109. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/model.py +0 -0
  110. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/planner.py +0 -0
  111. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/proof.py +0 -0
  112. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/review.py +0 -0
  113. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/scan.py +0 -0
  114. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/capture.py +0 -0
  115. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/cli.py +0 -0
  116. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/doc_import.py +0 -0
  117. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/doctor.py +0 -0
  118. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/domains.py +0 -0
  119. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/engine/__init__.py +0 -0
  120. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/engine/reader.py +0 -0
  121. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/gitio.py +0 -0
  122. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/host/__init__.py +0 -0
  123. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/host/claude_settings.py +0 -0
  124. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/importer.py +0 -0
  125. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/okf.py +0 -0
  126. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/profiles.py +0 -0
  127. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/retrieval.py +0 -0
  128. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/schema.py +0 -0
  129. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/server.py +0 -0
  130. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/stats/__init__.py +0 -0
  131. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/stats/model.py +0 -0
  132. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/stats/render.py +0 -0
  133. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/store.py +0 -0
  134. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/sync.py +0 -0
  135. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/verify.py +0 -0
  136. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/viz/__init__.py +0 -0
  137. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/viz/assets/vis-network.min.js +0 -0
  138. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/viz/model.py +0 -0
  139. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/viz/render.py +0 -0
  140. {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/viz/template.html +0 -0
  141. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/__init__.py +0 -0
  142. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/conftest.py +0 -0
  143. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/auto_policy/parity_goldens.json +0 -0
  144. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bitfinex_slice.json +0 -0
  145. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/flows/bmad/_bmad-output/planning-artifacts/architecture/cache/ARCHITECTURE-SPINE.md +0 -0
  146. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/flows/generic-adr/docs/adr/001-retry.md +0 -0
  147. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/flows/genkovich-sdd/docs/features/cache/adr/001.md +0 -0
  148. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/flows/spec-kit/specs/cache/plan.md +0 -0
  149. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/flows/superpowers/docs/superpowers/specs/2026-07-01-cache-design.md +0 -0
  150. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/graph.json +0 -0
  151. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/hosts/claude/.claude/settings.json +0 -0
  152. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/hosts/claude/.mcp.json +0 -0
  153. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/hosts/codex/.codex/config.toml +0 -0
  154. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/hosts/codex/.codex/hooks/hooks.json +0 -0
  155. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/scan/docs/adr/001-safe.md +0 -0
  156. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/corpus/PUBLIC_CORPORA.txt +0 -0
  157. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/corpus/openspec/_provenance.json +0 -0
  158. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/corpus/openspec/graph.json +0 -0
  159. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/corpus/self-corpus/_provenance.json +0 -0
  160. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/corpus/self-corpus/expected/retrieval-engine-reader.md +0 -0
  161. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/corpus/self-corpus/graph.json +0 -0
  162. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/adr/0001-use-sessions.md +0 -0
  163. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/bmad/ARCHITECTURE-SPINE.md +0 -0
  164. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/bmad/tree/_bmad-output/planning-artifacts/architecture/architecture-payments-2026-07-30/.memlog.md +0 -0
  165. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/bmad/tree/_bmad-output/planning-artifacts/architecture/architecture-payments-2026-07-30/ARCHITECTURE-SPINE.md +0 -0
  166. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/bmad/tree/_bmad-output/planning-artifacts/prds/prd-payments-2026-07-30/prd.md +0 -0
  167. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/genkovich/0001-queue-backpressure.md +0 -0
  168. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/genkovich/sad.md +0 -0
  169. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/genkovich/tree/docs/features/payments/adr/0001-queue-backpressure.md +0 -0
  170. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/genkovich/tree/docs/features/payments/sad.md +0 -0
  171. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/genkovich/tree/docs/features/payments/spec.md +0 -0
  172. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/design-empty.md +0 -0
  173. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/design-h3-split.md +0 -0
  174. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/design-with-summary.md +0 -0
  175. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/design.md +0 -0
  176. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/proposal-alternatives.md +0 -0
  177. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/proposal.md +0 -0
  178. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/changes/add-thing/design.md +0 -0
  179. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/changes/add-thing/proposal.md +0 -0
  180. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/changes/add-thing/specs/cap/spec.md +0 -0
  181. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/changes/add-thing/tasks.md +0 -0
  182. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/changes/archive/2026-01-01-add-thing/design.md +0 -0
  183. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/changes/archive/2026-01-01-add-thing/proposal.md +0 -0
  184. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/config.yaml +0 -0
  185. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/specs/cap/spec.md +0 -0
  186. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/spec-kit/plan.md +0 -0
  187. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/spec-kit/tree/specs/003-payment-retries/plan.md +0 -0
  188. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/spec-kit/tree/specs/003-payment-retries/spec.md +0 -0
  189. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/superpowers/2026-07-01-example-design.md +0 -0
  190. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/superpowers/2026-07-01-thin-design.md +0 -0
  191. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/mini_graph.json +0 -0
  192. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_anchoring.py +0 -0
  193. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_anchoring_mapping_refresh.py +0 -0
  194. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_apply.py +0 -0
  195. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_catalog.py +0 -0
  196. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_cli.py +0 -0
  197. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_integrations.py +0 -0
  198. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_model.py +0 -0
  199. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_planner.py +0 -0
  200. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_proof.py +0 -0
  201. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_review.py +0 -0
  202. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_scan.py +0 -0
  203. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_capture_auto_accept.py +0 -0
  204. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_capture_facts.py +0 -0
  205. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_capture_integration.py +0 -0
  206. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_capture_neighbors.py +0 -0
  207. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_capture_propose.py +0 -0
  208. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_capture_redact.py +0 -0
  209. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_compact.py +0 -0
  210. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_doctor.py +0 -0
  211. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_domains.py +0 -0
  212. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_import.py +0 -0
  213. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_init.py +0 -0
  214. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_okf.py +0 -0
  215. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_ratify.py +0 -0
  216. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_ratify_facts.py +0 -0
  217. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_stats.py +0 -0
  218. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_sync.py +0 -0
  219. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_sync_check.py +0 -0
  220. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_viz.py +0 -0
  221. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_codex_plugin.py +0 -0
  222. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_community_baseline.py +0 -0
  223. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_config.py +0 -0
  224. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_corpus_expected_renders.py +0 -0
  225. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_corpus_self_corpus.py +0 -0
  226. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_coverage_telemetry_e2e.py +0 -0
  227. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_digest_integrity.py +0 -0
  228. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doc_corpus_integration.py +0 -0
  229. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doc_import.py +0 -0
  230. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_docs_claims.py +0 -0
  231. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor.py +0 -0
  232. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_auto_share.py +0 -0
  233. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_code_drift.py +0 -0
  234. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_dangling_supports.py +0 -0
  235. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_duplicate_entity.py +0 -0
  236. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_graph_root_mismatch.py +0 -0
  237. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_never_surfaced.py +0 -0
  238. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_scan.py +0 -0
  239. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_stale_instructions.py +0 -0
  240. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_unratified_accept.py +0 -0
  241. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_domains.py +0 -0
  242. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_entity_duplicate_resolution.py +0 -0
  243. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_entity_get_or_create_race.py +0 -0
  244. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_env_isolation.py +0 -0
  245. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_fact_reachability_gate.py +0 -0
  246. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_flow_profile.py +0 -0
  247. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_git_bindings_blame.py +0 -0
  248. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_git_bindings_hook.py +0 -0
  249. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_git_bindings_provenance.py +0 -0
  250. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_claude_settings.py +0 -0
  251. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_session_start.py +0 -0
  252. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_importer.py +0 -0
  253. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_mind_model_additive_fields.py +0 -0
  254. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_okf.py +0 -0
  255. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_openspec_profile.py +0 -0
  256. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_pathless_descriptor_adoption.py +0 -0
  257. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_pilot_kit_corpus_fit.py +0 -0
  258. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_proposal_lifecycle.py +0 -0
  259. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_ratify_listing_gone_dark.py +0 -0
  260. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_ratify_policy.py +0 -0
  261. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_community_labels.py +0 -0
  262. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_doc_nodes.py +0 -0
  263. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_graph.py +0 -0
  264. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_integration.py +0 -0
  265. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_nodes_in_file.py +0 -0
  266. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_perf.py +0 -0
  267. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_rationale.py +0 -0
  268. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_read.py +0 -0
  269. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_resolve.py +0 -0
  270. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_subdir_mismatch.py +0 -0
  271. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_version.py +0 -0
  272. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_redaction_seeded.py +0 -0
  273. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_repoint_integration.py +0 -0
  274. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_budget_counters.py +0 -0
  275. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_context.py +0 -0
  276. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_drift_marker.py +0 -0
  277. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_drilldown.py +0 -0
  278. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_facts.py +0 -0
  279. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_ids.py +0 -0
  280. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_integration.py +0 -0
  281. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_rank.py +0 -0
  282. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_seeds.py +0 -0
  283. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_shown_ids.py +0 -0
  284. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_terminal_evidence.py +0 -0
  285. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_thin_tools.py +0 -0
  286. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_toc.py +0 -0
  287. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_toptier.py +0 -0
  288. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_unratified.py +0 -0
  289. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sandbox_hygiene.py +0 -0
  290. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_schema_descriptor.py +0 -0
  291. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_schema_domain.py +0 -0
  292. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_schema_fact.py +0 -0
  293. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_add_anchors.py +0 -0
  294. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_anchoring.py +0 -0
  295. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_capture.py +0 -0
  296. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_coverage_events.py +0 -0
  297. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_domains.py +0 -0
  298. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_drilldown.py +0 -0
  299. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_facts.py +0 -0
  300. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_find_entity.py +0 -0
  301. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_get_store.py +0 -0
  302. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_get_task_context.py +0 -0
  303. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_import.py +0 -0
  304. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_mcp_smoke.py +0 -0
  305. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_ratify_facts.py +0 -0
  306. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_redaction.py +0 -0
  307. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_render_events.py +0 -0
  308. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_supersede.py +0 -0
  309. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_sync_anchors.py +0 -0
  310. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_telemetry.py +0 -0
  311. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_thin_tools.py +0 -0
  312. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_verify.py +0 -0
  313. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_stats_graph.py +0 -0
  314. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_stats_model.py +0 -0
  315. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_stats_render.py +0 -0
  316. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_stats_skill.py +0 -0
  317. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_stats_snapshot.py +0 -0
  318. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store.py +0 -0
  319. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_atomic_write_tmp_names.py +0 -0
  320. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_canonical_stat.py +0 -0
  321. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_compact.py +0 -0
  322. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_compact_review.py +0 -0
  323. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_concurrent_open.py +0 -0
  324. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_coverage_telemetry.py +0 -0
  325. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_derived_community.py +0 -0
  326. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_domains.py +0 -0
  327. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_entities.py +0 -0
  328. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_fact_cascade.py +0 -0
  329. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_facts.py +0 -0
  330. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_meta.py +0 -0
  331. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_migration.py +0 -0
  332. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_mutation_guard.py +0 -0
  333. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_mutation_immediate.py +0 -0
  334. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_opens_with_merged_duplicate.py +0 -0
  335. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_persistence.py +0 -0
  336. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_ratification.py +0 -0
  337. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_rebuild_atomicity.py +0 -0
  338. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_render_events.py +0 -0
  339. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_retrieval.py +0 -0
  340. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_slug_conflicts.py +0 -0
  341. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_stamping_marker.py +0 -0
  342. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_telemetry.py +0 -0
  343. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_threading.py +0 -0
  344. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_tmp_sweep_age.py +0 -0
  345. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_clean.py +0 -0
  346. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_domains.py +0 -0
  347. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_drift_cache.py +0 -0
  348. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_fresh_clone.py +0 -0
  349. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_integration.py +0 -0
  350. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_rebind.py +0 -0
  351. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_repoint.py +0 -0
  352. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_report_has_findings.py +0 -0
  353. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_run.py +0 -0
  354. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_toc_cache.py +0 -0
  355. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_volatile_heal.py +0 -0
  356. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_wiring.py +0 -0
  357. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_telemetry_retention.py +0 -0
  358. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_verify_snapshot.py +0 -0
  359. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_verify_transitions.py +0 -0
  360. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_viz_asset_packaged.py +0 -0
  361. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_viz_model.py +0 -0
  362. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_viz_render_html.py +0 -0
  363. {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_viz_render_json.py +0 -0
@@ -7,6 +7,26 @@ interfaces, exactly, and what each one promises: [`docs/reference/stability.md`]
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.1] — 2026-09-19
11
+
12
+ ### Fixed
13
+
14
+ - **A session is now identified by its transcript, not by the host's `session_id`.** That
15
+ field does not mean the same thing on every host: Claude Code mints one per session (and
16
+ names the transcript after it), while Codex reports the *workspace* session — one id that
17
+ outlives a single session, survives `resume`, and is shared by every thread beneath it. On
18
+ Codex that put a whole store's history in one bucket (measured: 1928 of 1929 recorded events
19
+ in one live store, spanning 27 hours) and, worse, made every per-session guard fire once per
20
+ workspace instead of once per session: the second thread opened within a minute had its
21
+ context map dropped as a duplicate injection, and the first thread to finish spent the
22
+ capture nudge for all of them. All four hook sites now take the session from
23
+ `transcript_path`'s file name, falling back to `session_id` when no transcript is given.
24
+ On Claude Code this is the same string it already recorded — verified against a live store,
25
+ where all 74 recorded session ids are exactly transcript names — so nothing moves there.
26
+ The workspace session is kept beside it in the new `telemetry:session_group` meta key, as
27
+ the only link between sibling threads. See
28
+ [`docs/reference/hooks.md`](docs/reference/hooks.md#which-session-a-hook-is-in).
29
+
10
30
  ## [0.3.0] — 2026-09-19
11
31
 
12
32
  ### Added
@@ -13,7 +13,7 @@ task-aware retrieval + decision memory riding on the engine's entity graph.
13
13
  continuing participant in the project. A core feature must preserve, organize, or deliver
14
14
  accumulated project understanding. Document search by itself is not the product.
15
15
 
16
- Sidegraph is pre-1.0 (v0.3.0) — the full loop (capture, ratification, mistakes-first
16
+ Sidegraph is pre-1.0 (v0.3.1) — the full loop (capture, ratification, mistakes-first
17
17
  retrieval, refactor-surviving re-anchoring, semantic docs layer, mind-model domains, and the
18
18
  facts evidence layer) ships and is exercised end-to-end, but interfaces may still move before
19
19
  a stable release. See
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sidegraph
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Own the memory, rent the graph — a durable decision/lessons layer as a sidecar over a code-graph engine.
5
5
  Project-URL: Homepage, https://github.com/SantyagoSeaman/sidegraph
6
6
  Project-URL: Repository, https://github.com/SantyagoSeaman/sidegraph
@@ -7,6 +7,24 @@ a JSON payload from stdin and write a JSON response to stdout, per the Claude Co
7
7
  contract. For wiring them into `.claude/settings.json`, see
8
8
  [`getting-started/claude-code-setup.md`](../getting-started/claude-code-setup.md).
9
9
 
10
+ ## Which session a hook is in
11
+
12
+ Every per-session guard below — the capture ledger, the nudge markers, the recorded events —
13
+ needs to know which session it is in, and the payload field named `session_id` does not mean
14
+ the same thing on every host:
15
+
16
+ - **Claude Code** gives a fresh `session_id` per session, and names the transcript after it.
17
+ - **Codex** gives the *workspace* session: one id that outlives a single session, survives
18
+ `resume`, and is shared by every thread under that workspace.
19
+
20
+ So the session is taken from **`transcript_path` — its file name without the extension** —
21
+ falling back to `session_id` when the payload carries no transcript (the field is nullable in
22
+ Codex's own hook schema). On Claude Code the two are the same string, so nothing changed
23
+ there; on Codex each thread is now its own session instead of a day of work sharing one.
24
+
25
+ The workspace session, when it differs, is kept separately in the `telemetry:session_group`
26
+ meta key: it is the only thing linking sibling threads.
27
+
10
28
  ## `sidegraph-session-start`
11
29
 
12
30
  Reads: `$SIDEGRAPH_DIR` (or the deprecated `$SIDEGRAPH_DB`; default `.sidegraph`, resolved via
@@ -19,8 +37,9 @@ single proposal-surfacing policy — `$SIDEGRAPH_UNRATIFIED` and
19
37
  [`configuration.md`](configuration.md#environment-variables)): proposed records outside
20
38
  the surfacing window, or any proposed record in regulated mode, no longer render as
21
39
  content in the TOC's unratified block, while the pending-ratification counter below
22
- always keeps counting them. The stdin payload is read but its
23
- contents are currently unused.
40
+ always keeps counting them. From the stdin payload it reads only the session — see
41
+ [Which session a hook is in](#which-session-a-hook-is-in) — used for the duplicate-injection
42
+ guard and published to the store as the telemetry session key.
24
43
 
25
44
  Behavior:
26
45
 
@@ -120,7 +139,8 @@ Reads: `$SIDEGRAPH_DIR` (or the deprecated `$SIDEGRAPH_DB`; default `.sidegraph`
120
139
  `$SIDEGRAPH_CAPTURE_NUDGE` (unset by default; set to `off` to disable this hook entirely).
121
140
  Reads the stdin JSON payload for three keys: `session_id`, `stop_hook_active`, and
122
141
  `transcript_path` (the Claude Code Stop-hook contract — `transcript_path` points at the
123
- session's transcript JSONL file). Does not read `$SIDEGRAPH_GRAPH` — this hook never touches
142
+ session's transcript JSONL file, and also identifies the session: see
143
+ [Which session a hook is in](#which-session-a-hook-is-in)). Does not read `$SIDEGRAPH_GRAPH` — this hook never touches
124
144
  the graph.
125
145
 
126
146
  Behavior, in order:
@@ -128,8 +148,8 @@ Behavior, in order:
128
148
  1. `$SIDEGRAPH_CAPTURE_NUDGE == "off"` → print `{}` (allow), without marking the ledger.
129
149
  2. `stop_hook_active` truthy (this Stop is the continuation of our own earlier block) →
130
150
  print `{}` (allow). Prevents an infinite block loop.
131
- 3. No `session_id` in the payload → print `{}` (allow) — can't dedup the nudge without an id,
132
- so it never risks looping.
151
+ 3. No session identity in the payload (neither `transcript_path` nor `session_id`) → print
152
+ `{}` (allow) — can't dedup the nudge without an id, so it never risks looping.
133
153
  4. **Substance gate:** count the transcript's *real* user prompts — JSONL lines with
134
154
  `type: "user"` whose `message.content` is a string, or a list containing at least one
135
155
  block that is NOT `type: "tool_result"`. Claude Code transcripts use `type: "user"` for
@@ -187,7 +207,7 @@ The banner appears **at most once per session**, and only in sessions that have
187
207
  triggers it.
188
208
 
189
209
  **Once-per-session behavior:** the `capture_sessions` table in the store (`was_captured` /
190
- `mark_captured`) is the ledger — one row per `session_id`. Combined with the
210
+ `mark_captured`) is the ledger — one row per session. Combined with the
191
211
  `stop_hook_active` check and the substance gate, this guarantees the block-and-nudge fires
192
212
  **at most once per session**, and only once the session looks substantial, even though Claude
193
213
  Code calls `Stop` every time the agent finishes responding. Ordering matters:
@@ -205,7 +225,8 @@ Reads: `$SIDEGRAPH_DIR` (or the deprecated `$SIDEGRAPH_DB`; default `.sidegraph`
205
225
  `$SIDEGRAPH_GREP_NUDGE` (unset by default; set to `off` to disable this hook entirely). Does
206
226
  not read `$SIDEGRAPH_GRAPH` — this hook, like
207
227
  `stop`, never touches the graph. Reads the stdin JSON payload for three keys: `tool_name`,
208
- `tool_input`, `session_id`.
228
+ `tool_input`, and the session (`transcript_path`, else `session_id` — see
229
+ [Which session a hook is in](#which-session-a-hook-is-in)).
209
230
 
210
231
  Behavior, in order — any `{}` below means the tool call proceeds through Claude Code's normal
211
232
  permission flow, untouched:
@@ -216,8 +237,8 @@ permission flow, untouched:
216
237
  3. `tool_input` has no string `file_path`/`path`/`pattern` argument (and no other non-empty
217
238
  string argument at all) → print `{}`. Deliberately permissive otherwise — any string arg is
218
239
  treated as "looks like a file/pattern target," not just a particular path shape.
219
- 4. No `session_id` in the payload → print `{}`.
220
- 5. A per-session marker (`pretool_nudge:<session_id>` in `store.meta` — deliberately **not**
240
+ 4. No session identity in the payload → print `{}`.
241
+ 5. A per-session marker (`pretool_nudge:<session>` in `store.meta` — deliberately **not**
221
242
  the `Stop` hook's `capture_sessions` ledger, so the two one-shot guards can't consume each
222
243
  other) already set → print `{}`.
223
244
  6. The store has zero accepted domains **and** zero currently-valid (non-superseded,
@@ -45,6 +45,15 @@ package version.
45
45
 
46
46
  Run from a clean `main`, in this order.
47
47
 
48
+ 0. `uv run python tools/preflight_release.py`. Seconds, no network. It checks what the rest
49
+ of this checklist cannot: that every restatement of the version agrees with
50
+ `pyproject.toml`, that the changelog carries a dated heading with a fresh `[Unreleased]`
51
+ above it, that both checkouts are clean and on `main`, that the tag does not already
52
+ exist — and, above all, **which repository the tag belongs to**, derived from the
53
+ checkout rather than assumed. Add `--online` for the two remote lookups. It reports every
54
+ finding in one run rather than stopping at the first, so a release is prepared in one
55
+ sitting instead of five.
56
+
48
57
  1. `uv run pytest -q`. Full suite green.
49
58
  2. `uv run pre-commit run --all-files`. Lint, format, and types clean; this is exactly what
50
59
  CI runs, and now also gitleaks, zizmor, actionlint, and the shipped-surface/public-twin/
@@ -80,6 +89,13 @@ one. Cut the release with:
80
89
  git tag vX.Y.Z && git push origin vX.Y.Z
81
90
  ```
82
91
 
92
+ **Run that in the public checkout, not the internal one.** PyPI's trusted publisher is
93
+ configured against `SantyagoSeaman/sidegraph`, so a tag pushed to the internal repository
94
+ produces a valid OIDC token with the wrong `repository` claim and the publish job dies on
95
+ `invalid-publisher` — after the whole test matrix has run. The public snapshot is therefore
96
+ cut and pushed *first*, and the tag goes on the commit that snapshot created. Pushing the
97
+ internal `main` is a separate step that triggers no publish at all.
98
+
83
99
  That push triggers [`.github/workflows/publish.yml`](../../.github/workflows/publish.yml),
84
100
  three jobs in sequence.
85
101
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sidegraph",
3
3
  "displayName": "Sidegraph",
4
- "version": "0.3.0",
4
+ "version": "0.3.1",
5
5
  "description": "Durable decision/lessons memory over your code graph — ADRs, gotchas, and constraints that survive refactors, retrieved mistakes-first.",
6
6
  "author": { "name": "SantyagoSeaman", "url": "https://github.com/SantyagoSeaman" },
7
7
  "homepage": "https://github.com/SantyagoSeaman/sidegraph",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidegraph",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Durable decision/lessons memory over your code graph — ADRs, gotchas, and constraints that survive refactors, retrieved mistakes-first.",
5
5
  "license": "Apache-2.0",
6
6
  "skills": "./skills/",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sidegraph"
3
- version = "0.3.0"
3
+ version = "0.3.1"
4
4
  description = "Own the memory, rent the graph — a durable decision/lessons layer as a sidecar over a code-graph engine."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -19,7 +19,7 @@ from .schema import (
19
19
  )
20
20
  from .store import Store
21
21
 
22
- __version__ = "0.3.0"
22
+ __version__ = "0.3.1"
23
23
 
24
24
  __all__ = [
25
25
  "SCHEMA_VERSION",
@@ -87,6 +87,14 @@ def _warn_deprecated_sidegraph_db(value: str) -> None:
87
87
  # without importing the host seam — host/hooks.py re-exports it for its own callers/tests.
88
88
  TELEMETRY_SESSION_KEY = "telemetry:session"
89
89
 
90
+ # The workspace session a recorded session sits beneath, when the host has one (Codex: its
91
+ # `session_id` is an umbrella that spans days, survives resume and covers every thread under
92
+ # it — see host/hooks._session_identity). Written only when it differs from the recorded key,
93
+ # so a Claude Code store never carries it: there, the two are the same string. Kept because it
94
+ # is the only link between sibling threads, which is what separates "how many tasks" from
95
+ # "how many agents under one task" on that host.
96
+ TELEMETRY_SESSION_GROUP_KEY = "telemetry:session_group"
97
+
90
98
 
91
99
  def telemetry_enabled() -> bool:
92
100
  """True unless ``SIDEGRAPH_TELEMETRY=off``. Opt-out, read at point of use and never
@@ -23,13 +23,14 @@ import json
23
23
  import os
24
24
  import sys
25
25
  from datetime import UTC, datetime
26
+ from pathlib import PurePosixPath
26
27
  from typing import NamedTuple
27
28
 
28
29
  # Defined in config.py, not here: server.py (core) needs this constant too (Task 4), and
29
30
  # core importing from host/ would invert this project's seam rule (host may depend on the
30
31
  # core, never the reverse). Re-exported so `hooks.TELEMETRY_SESSION_KEY` still resolves for
31
32
  # callers and tests that reach it through this module.
32
- from ..config import TELEMETRY_SESSION_KEY
33
+ from ..config import TELEMETRY_SESSION_GROUP_KEY, TELEMETRY_SESSION_KEY
33
34
 
34
35
  CAPTURE_NUDGE = (
35
36
  "Sidegraph: if this session produced a durable decision, lesson, or gotcha — or a "
@@ -68,6 +69,41 @@ _SESSION_START_KEY = "session_start"
68
69
  _SESSION_START_DEDUPE_SECONDS = 60
69
70
 
70
71
 
72
+ def _session_identity(payload: dict) -> tuple[str | None, str | None]:
73
+ """The session this payload belongs to, and the workspace session above it.
74
+
75
+ ``payload["session_id"]`` does not mean the same thing on both hosts, and reading it
76
+ directly attributed a Codex store's whole history to one session. Measured 2026-09-19:
77
+
78
+ - Claude Code: per session, and the transcript is named after it — all 74 session ids in
79
+ this repository's own store are exactly transcript stems.
80
+ - Codex: the *umbrella* workspace session. It spans days, survives ``resume``, and covers
81
+ every thread beneath it — 1928 of 1929 events in a second live store landed in one
82
+ bucket across 27 hours. The thread's own identity is its rollout file; Codex's
83
+ ``session-start.command.input`` schema carries no thread id and no ``agent_type``.
84
+
85
+ So the transcript path is the one field that identifies a session on both hosts, and its
86
+ stem is the id on the host where the two agree. Returns ``(key, group)``: ``group`` is the
87
+ host's own ``session_id`` when it differs from ``key``, else ``None`` — nothing extra to
88
+ record on a host whose session id already IS the key.
89
+
90
+ Beyond attribution, this is what unblocks the 60s double-injection dedupe
91
+ (:func:`_session_start_duplicate`): two Codex threads opened within a minute reported the
92
+ same id, so the second one's context map was suppressed as a duplicate. Observed in the
93
+ live rollouts, which start in pairs seconds apart.
94
+ """
95
+ raw_id = payload.get("session_id")
96
+ session_id = raw_id if isinstance(raw_id, str) and raw_id else None
97
+ raw_path = payload.get("transcript_path")
98
+ stem = None
99
+ if isinstance(raw_path, str) and raw_path:
100
+ # PurePosixPath, not Path: the stem of a host-written path, never touched on disk.
101
+ stem = PurePosixPath(raw_path).stem or None
102
+ key = stem or session_id
103
+ group = session_id if session_id and session_id != key else None
104
+ return key, group
105
+
106
+
71
107
  def _session_start_duplicate(store, session_id: str, now: datetime) -> bool:
72
108
  """True iff ``session_start()`` already ran for ``session_id`` within the last
73
109
  :data:`_SESSION_START_DEDUPE_SECONDS` (design D7.2). Same session id AND a fresh
@@ -245,24 +281,22 @@ def session_start() -> None:
245
281
 
246
282
  payload = _read_payload()
247
283
  store = Store(resolve_store_path(root=os.environ.get("CLAUDE_PROJECT_DIR")))
248
- session_id = payload.get("session_id")
284
+ # Not payload["session_id"] directly: that field is the umbrella workspace session
285
+ # on Codex, not this session — see _session_identity.
286
+ session_id, session_group = _session_identity(payload)
249
287
 
250
288
  # D7.2: double-injection dedupe, checked BEFORE any other work (sync/TOC/nudges) —
251
289
  # a duplicate SessionStart call for the same session within the window exits
252
290
  # silently, same "print {} and return" shape the Stop hook's own one-shot ledger
253
291
  # uses. Gated on a real session id (see _session_start_duplicate's docstring).
254
- if (
255
- isinstance(session_id, str)
256
- and session_id
257
- and _session_start_duplicate(store, session_id, datetime.now(UTC))
258
- ):
292
+ if session_id and _session_start_duplicate(store, session_id, datetime.now(UTC)):
259
293
  print(json.dumps({}))
260
294
  return
261
295
 
262
296
  # Own try/except, like the ratification nudge below: telemetry must never cost the
263
297
  # map, which is this hook's only real deliverable.
264
298
  try:
265
- if isinstance(session_id, str) and session_id:
299
+ if session_id:
266
300
  # D7.3 (staleness-machinery wave, E8: author=None session=None on
267
301
  # Stop-channel captures): this write is now UNCONDITIONAL — no longer
268
302
  # gated on telemetry_enabled() — because capture._propose_one falls back
@@ -272,6 +306,8 @@ def session_start() -> None:
272
306
  TELEMETRY_SESSION_KEY,
273
307
  f"{session_id}|{datetime.now(UTC).isoformat()}",
274
308
  )
309
+ if session_group:
310
+ store.set_meta(TELEMETRY_SESSION_GROUP_KEY, session_group)
275
311
  # Pruning is deliberately NOT gated on telemetry_enabled() (practitioner
276
312
  # re-review round 2): it used to be, which meant opting out froze the 30-day
277
313
  # retention of whatever journal already existed — an opt-out that makes the
@@ -416,7 +452,7 @@ def stop() -> None:
416
452
  if payload.get("stop_hook_active"):
417
453
  print(json.dumps({}))
418
454
  return
419
- session_id = payload.get("session_id")
455
+ session_id, _ = _session_identity(payload)
420
456
  if not session_id:
421
457
  print(json.dumps({}))
422
458
  return
@@ -540,8 +576,8 @@ def _record_touch_event(payload: dict) -> None:
540
576
  tool = payload.get("tool_name")
541
577
  if tool not in _TOUCH_TOOLS:
542
578
  return
543
- session_id = payload.get("session_id")
544
- if not isinstance(session_id, str) or not session_id:
579
+ session_id, _ = _session_identity(payload)
580
+ if not session_id:
545
581
  return
546
582
  root = os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
547
583
  path = _touch_path(payload.get("tool_input"), root)
@@ -684,7 +720,7 @@ def pre_tool_use() -> None:
684
720
  if not _looks_like_source_target(payload.get("tool_input")):
685
721
  print(json.dumps({}))
686
722
  return
687
- session_id = payload.get("session_id")
723
+ session_id, _ = _session_identity(payload)
688
724
  if not session_id:
689
725
  print(json.dumps({}))
690
726
  return
@@ -41,8 +41,11 @@ def _assistant(text="ok"):
41
41
  }
42
42
 
43
43
 
44
- def _transcript(tmp_path, real_prompts=2):
45
- path = tmp_path / "transcript.jsonl"
44
+ def _transcript(tmp_path, real_prompts=2, name="s1"):
45
+ # Named after the session, as both hosts do (Claude Code: `<session_id>.jsonl`; Codex:
46
+ # the per-thread rollout file) — hooks._session_identity takes the session from this
47
+ # path, so two sessions sharing one file name would share their per-session ledgers.
48
+ path = tmp_path / f"{name}.jsonl"
46
49
  with open(path, "w") as fh:
47
50
  for _ in range(real_prompts):
48
51
  fh.write(json.dumps(_user_prompt()) + "\n")
@@ -123,7 +126,7 @@ def test_session_start_survives_refresh_raising(tmp_path, monkeypatch, capsys):
123
126
 
124
127
 
125
128
  def _stop_payload(tmp_path, session="s1"):
126
- return {"session_id": session, "transcript_path": _transcript(tmp_path)}
129
+ return {"session_id": session, "transcript_path": _transcript(tmp_path, name=session)}
127
130
 
128
131
 
129
132
  @pytest.mark.parametrize("n", [5, 10**6])
@@ -175,12 +178,12 @@ def test_stop_refresh_runs_after_substance_gate_only(tmp_path, monkeypatch, caps
175
178
  refresh calls — no git work on trivial sessions — and a substantial one makes ≥1.
176
179
  The ≥1 half is the honest red half against unfixed code."""
177
180
  calls = _fake_refresh(monkeypatch, 1)
178
- gated = {"session_id": "s-gated", "transcript_path": _transcript(tmp_path, real_prompts=1)}
181
+ gated = {"session_id": "s-gated", "transcript_path": _transcript(tmp_path, 1, "s-gated")}
179
182
  out = _run_stop(monkeypatch, capsys, gated, tmp_path / ".sidegraph")
180
183
  assert out == {}
181
184
  assert len(calls) == 0
182
185
 
183
- substantial = {"session_id": "s-full", "transcript_path": _transcript(tmp_path)}
186
+ substantial = {"session_id": "s-full", "transcript_path": _transcript(tmp_path, name="s-full")}
184
187
  out = _run_stop(monkeypatch, capsys, substantial, tmp_path / ".sidegraph")
185
188
  assert out["decision"] == "block"
186
189
  assert len(calls) == 1
@@ -344,3 +344,36 @@ def test_title_is_clipped_and_sanitised(tmp_path, monkeypatch, capsys):
344
344
  assert "\n" not in text
345
345
  assert text.count('"') == 2 # exactly the pair this nudge adds
346
346
  assert "…" in text
347
+
348
+
349
+ # -- Codex: one umbrella session id, several threads ----------------------------------------
350
+ #
351
+ # Codex's payload `session_id` is the workspace session, not this session: it spans days and
352
+ # covers every thread under it (see hooks._session_identity). The one-shot ledger is keyed on
353
+ # the session, so reading that field directly spent the nudge for a whole workspace on the
354
+ # first thread that read a file.
355
+
356
+ _CODEX_ROLLOUTS = (
357
+ "/w/.codex/sessions/2026/09/19/rollout-2026-09-19T12-00-31-01a0b953-2d7c.jsonl",
358
+ "/w/.codex/sessions/2026/09/19/rollout-2026-09-19T12-16-06-01a0b961-7463.jsonl",
359
+ )
360
+
361
+
362
+ def test_each_codex_thread_gets_its_own_nudge(tmp_path, monkeypatch, capsys):
363
+ """Red against a ledger keyed on payload['session_id']: the second thread — a different
364
+ session reporting the same umbrella id — was silently treated as already nudged."""
365
+ db = tmp_path / "s.db"
366
+ _seed_one_decision(db)
367
+ umbrella = "01a0b3e2-39d2-7180-86a5-408a6f9ce058"
368
+
369
+ outs = [
370
+ _run(
371
+ monkeypatch,
372
+ capsys,
373
+ dict(READ_PAYLOAD, session_id=umbrella, transcript_path=rollout),
374
+ db,
375
+ )
376
+ for rollout in _CODEX_ROLLOUTS
377
+ ]
378
+
379
+ assert all("hookSpecificOutput" in out for out in outs), outs
@@ -144,3 +144,96 @@ def test_a_prune_failure_does_not_cost_the_session_map(tmp_path, monkeypatch, ca
144
144
  out = json.loads(capsys.readouterr().out)
145
145
 
146
146
  assert "hookSpecificOutput" in out, "the map must survive a telemetry failure"
147
+
148
+
149
+ # -- Codex: session identity comes from the transcript, not the host's session_id ----------
150
+ #
151
+ # Measured 2026-09-19 on two live stores. Claude Code's `session_id` is per session and its
152
+ # transcript is named after it: all 74 session ids in this repo's own store are exactly
153
+ # transcript stems. Codex's `session_id` is the umbrella workspace session — it spans days,
154
+ # survives resume, and covers every thread beneath it: 1928 of 1929 events in a second live
155
+ # store landed in ONE bucket across 27 hours, while each thread had its own rollout file.
156
+ # Codex's SessionStart payload carries no thread id and no agent_type (schema
157
+ # `session-start.command.input`), so `transcript_path` is the only field that identifies a
158
+ # session on both hosts.
159
+
160
+
161
+ def _codex_payload(rollout: str, umbrella: str = "01a0b3e2-39d2-7180-86a5-408a6f9ce058"):
162
+ """A Codex SessionStart payload: one umbrella id, one per-thread rollout file."""
163
+ return {
164
+ "session_id": umbrella,
165
+ "source": "startup",
166
+ "transcript_path": f"/w/.codex/sessions/2026/09/19/{rollout}.jsonl",
167
+ "cwd": "/w/project",
168
+ }
169
+
170
+
171
+ def test_two_codex_threads_sharing_one_umbrella_id_are_two_sessions(tmp_path, monkeypatch, capsys):
172
+ """The defect, red against reading payload['session_id']: two Codex threads report the
173
+ same umbrella `session_id`, so every event from both was attributed to one session and
174
+ the second thread's SessionStart was swallowed by the 60s double-injection dedupe."""
175
+ db = tmp_path / "db"
176
+ keys = []
177
+ for rollout in (
178
+ "rollout-2026-09-19T12-00-31-01a0b953-2d7c-76e3-9dac-7d375dad7954",
179
+ "rollout-2026-09-19T12-16-06-01a0b961-7463-7b21-bd3b-bab64d9d5d8b",
180
+ ):
181
+ _run_session_start(monkeypatch, capsys, _codex_payload(rollout), db)
182
+ store = Store(db)
183
+ raw = store.get_meta(hooks.TELEMETRY_SESSION_KEY)
184
+ store.close()
185
+ assert raw is not None
186
+ keys.append(raw.partition("|")[0])
187
+
188
+ assert keys[0] != keys[1], "two threads under one umbrella id must be two sessions"
189
+ assert keys[1].endswith("01a0b961-7463-7b21-bd3b-bab64d9d5d8b")
190
+
191
+
192
+ def test_the_umbrella_id_is_kept_so_codex_threads_can_be_grouped(tmp_path, monkeypatch, capsys):
193
+ """The workspace session is the only link between a Codex thread and its siblings, so it
194
+ is recorded beside the key rather than discarded."""
195
+ db = tmp_path / "db"
196
+ _run_session_start(
197
+ monkeypatch, capsys, _codex_payload("rollout-2026-09-19T12-16-06-01a0b961-7463"), db
198
+ )
199
+
200
+ store = Store(db)
201
+ group = store.get_meta(hooks.TELEMETRY_SESSION_GROUP_KEY)
202
+ store.close()
203
+
204
+ assert group == "01a0b3e2-39d2-7180-86a5-408a6f9ce058"
205
+
206
+
207
+ def test_claude_code_keeps_the_session_id_it_already_recorded(tmp_path, monkeypatch, capsys):
208
+ """No-regression control, and the reason this change is safe to ship: under Claude Code
209
+ the transcript stem IS the session id, so the recorded key does not move and no group
210
+ key is written (there is no umbrella above a Claude Code session)."""
211
+ db = tmp_path / "db"
212
+ sid = "134960fe-2641-48d5-aaf7-223d9129df38"
213
+ _run_session_start(
214
+ monkeypatch,
215
+ capsys,
216
+ {"session_id": sid, "transcript_path": f"/w/.claude/projects/repo/{sid}.jsonl"},
217
+ db,
218
+ )
219
+
220
+ store = Store(db)
221
+ raw = store.get_meta(hooks.TELEMETRY_SESSION_KEY)
222
+ group = store.get_meta(hooks.TELEMETRY_SESSION_GROUP_KEY)
223
+ store.close()
224
+
225
+ assert raw is not None and raw.partition("|")[0] == sid
226
+ assert group is None
227
+
228
+
229
+ def test_a_null_transcript_path_falls_back_to_the_session_id(tmp_path, monkeypatch, capsys):
230
+ """`transcript_path` is nullable in Codex's own schema, so its absence must leave the
231
+ previous behaviour exactly as it was rather than record nothing."""
232
+ db = tmp_path / "db"
233
+ _run_session_start(monkeypatch, capsys, {"session_id": "abc-123", "transcript_path": None}, db)
234
+
235
+ store = Store(db)
236
+ raw = store.get_meta(hooks.TELEMETRY_SESSION_KEY)
237
+ store.close()
238
+
239
+ assert raw is not None and raw.partition("|")[0] == "abc-123"
@@ -73,7 +73,8 @@ def _substantial(tmp_path, name="transcript.jsonl"):
73
73
 
74
74
  def test_first_stop_blocks_with_nudge_and_marks_ledger(tmp_path, monkeypatch, capsys):
75
75
  db = tmp_path / "s.db"
76
- transcript = _substantial(tmp_path)
76
+ # Named after the session, as both hosts do — the ledger key comes from this path.
77
+ transcript = _substantial(tmp_path, name="s1.jsonl")
77
78
  out = _run(
78
79
  monkeypatch,
79
80
  capsys,
@@ -273,7 +274,7 @@ def test_one_shot_session_with_tool_work_arms_the_gate(tmp_path, monkeypatch, ca
273
274
  [_user_prompt()] + [_assistant_tool_use() for _ in range(hooks._MIN_TOOL_USES)],
274
275
  "sdk-cli",
275
276
  )
276
- transcript = _write_transcript(tmp_path / "transcript.jsonl", entries)
277
+ transcript = _write_transcript(tmp_path / "os1.jsonl", entries)
277
278
  out = _run(
278
279
  monkeypatch,
279
280
  capsys,
@@ -707,3 +708,31 @@ def test_stop_hook_honors_legacy_sidegraph_db_with_deprecation_notice(
707
708
 
708
709
  assert "SIDEGRAPH_DB is deprecated" in captured.err
709
710
  assert "SIDEGRAPH_DIR" in captured.err
711
+
712
+
713
+ def test_each_codex_thread_earns_its_own_capture_nudge(tmp_path, monkeypatch, capsys):
714
+ """The capture ledger is per session, and Codex reports one umbrella `session_id` for
715
+ every thread under a workspace (hooks._session_identity). Red against reading that field
716
+ directly: the first thread to finish spent the nudge for all of them — a day of Codex
717
+ threads got one capture prompt between them."""
718
+ db = tmp_path / "s.db"
719
+ umbrella = "01a0b3e2-39d2-7180-86a5-408a6f9ce058"
720
+ outs = []
721
+ for thread in (
722
+ "rollout-2026-09-19T12-00-31-01a0b953-2d7c",
723
+ "rollout-2026-09-19T12-16-06-01a0b961-7463",
724
+ ):
725
+ outs.append(
726
+ _run(
727
+ monkeypatch,
728
+ capsys,
729
+ {
730
+ "session_id": umbrella,
731
+ "stop_hook_active": False,
732
+ "transcript_path": _substantial(tmp_path, name=f"{thread}.jsonl"),
733
+ },
734
+ db,
735
+ )
736
+ )
737
+
738
+ assert [out.get("decision") for out in outs] == ["block", "block"], outs
@@ -251,3 +251,28 @@ def test_a_store_failure_does_not_break_the_hook(tmp_path, monkeypatch, capsys):
251
251
  root,
252
252
  )
253
253
  assert isinstance(out, dict) # valid JSON, hook did not crash
254
+
255
+
256
+ def test_a_codex_touch_is_recorded_against_its_thread_not_the_workspace(
257
+ tmp_path, monkeypatch, capsys
258
+ ):
259
+ """Codex's payload `session_id` is the umbrella workspace session — stable across days and
260
+ every thread beneath it (hooks._session_identity). Recording against it put a whole
261
+ store's history in one bucket: 1928 of 1929 events in a live store, across 27 hours.
262
+ Red against reading that field directly."""
263
+ db = tmp_path / "db"
264
+ root = tmp_path
265
+ (root / "a.py").write_text("x = 1\n")
266
+ umbrella = "01a0b3e2-39d2-7180-86a5-408a6f9ce058"
267
+ thread = "rollout-2026-09-19T12-16-06-01a0b961-7463"
268
+ payload = {
269
+ "session_id": umbrella,
270
+ "tool_name": "Read",
271
+ "tool_input": {"file_path": str(root / "a.py")},
272
+ "transcript_path": f"/w/.codex/sessions/2026/09/19/{thread}.jsonl",
273
+ }
274
+
275
+ _run(monkeypatch, capsys, payload, db, root)
276
+
277
+ assert _events(db, umbrella) == []
278
+ assert len(_events(db, thread)) == 1
@@ -1293,7 +1293,7 @@ wheels = [
1293
1293
 
1294
1294
  [[package]]
1295
1295
  name = "sidegraph"
1296
- version = "0.3.0"
1296
+ version = "0.3.1"
1297
1297
  source = { editable = "." }
1298
1298
  dependencies = [
1299
1299
  { name = "fastmcp" },
File without changes
File without changes
File without changes
File without changes
File without changes