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.
- {sidegraph-0.3.0 → sidegraph-0.3.1}/CHANGELOG.md +20 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/CLAUDE.md +1 -1
- {sidegraph-0.3.0 → sidegraph-0.3.1}/PKG-INFO +1 -1
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/hooks.md +30 -9
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/releasing.md +16 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/.claude-plugin/plugin.json +1 -1
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/.codex-plugin/plugin.json +1 -1
- {sidegraph-0.3.0 → sidegraph-0.3.1}/pyproject.toml +1 -1
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/__init__.py +1 -1
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/config.py +8 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/host/hooks.py +48 -12
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_drift_nudge.py +8 -5
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_pretool.py +33 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_session_key.py +93 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_stop.py +31 -2
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_touch_events.py +25 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/uv.lock +1 -1
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.agents/plugins/marketplace.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.claude-plugin/marketplace.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/CODEOWNERS +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/secret_scanning.yml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/workflows/ci.yml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/workflows/publish.yml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.github/workflows/scorecard.yml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.gitignore +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.gitleaks.toml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.pre-commit-config.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/.python-version +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/AGENTS.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/CODE_OF_CONDUCT.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/CONTRIBUTING.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/LICENSE +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/README.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/SECURITY.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/README.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/concepts/anchoring.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/concepts/data-model.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/concepts/decision-memory.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/concepts/mind-model.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/concepts/retrieval.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/getting-started/bootstrap.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/getting-started/claude-code-setup.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/getting-started/codex-setup.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/getting-started/installation.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/getting-started/quickstart.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/capturing-decisions.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/ci-cd-maintenance.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/naming-your-domains.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/retrieval-in-sessions.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/semantic-docs.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/surviving-refactors.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/team-workflow.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/guides/verifying-your-setup.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/integrations/claude-code.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/integrations/codex.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/integrations/graphify.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/llms.txt +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/pilot-kit/README.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/pilot-kit/corpus_fit.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/pilot-kit/judge-prompt.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/pilot-kit/questions-prompt.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/pilot-kit/rubric-template.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/cli.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/configuration.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/git-bindings.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/mcp-tools.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/operations.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/stability.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/reference/store-format.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/docs/whitepaper/index.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/.mcp.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/codex/hooks.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/codex/mcp.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/hooks/hooks.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/check-plan/SKILL.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/check-plan/agents/openai.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/explain-why/SKILL.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/explain-why/agents/openai.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/heal-anchors/SKILL.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/heal-anchors/agents/openai.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/import-adrs/SKILL.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/import-adrs/agents/openai.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/manage-domains/SKILL.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/manage-domains/agents/openai.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/name-domains/SKILL.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/name-domains/agents/openai.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/ratify-decisions/SKILL.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/ratify-decisions/agents/openai.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/record-decision/SKILL.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/record-decision/agents/openai.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/record-fact/SKILL.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/record-fact/agents/openai.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/setup/SKILL.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/setup/agents/openai.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/stats/SKILL.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/stats/agents/openai.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/triage-drift/SKILL.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/plugin/sidegraph/skills/triage-drift/agents/openai.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/anchoring.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/__init__.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/apply.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/catalog.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/cli.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/integrations.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/model.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/planner.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/proof.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/review.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/bootstrap/scan.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/capture.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/cli.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/doc_import.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/doctor.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/domains.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/engine/__init__.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/engine/reader.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/gitio.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/host/__init__.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/host/claude_settings.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/importer.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/okf.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/profiles.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/retrieval.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/schema.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/server.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/stats/__init__.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/stats/model.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/stats/render.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/store.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/sync.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/verify.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/viz/__init__.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/viz/assets/vis-network.min.js +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/viz/model.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/viz/render.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/src/sidegraph/viz/template.html +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/__init__.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/conftest.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/auto_policy/parity_goldens.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bitfinex_slice.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/flows/bmad/_bmad-output/planning-artifacts/architecture/cache/ARCHITECTURE-SPINE.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/flows/generic-adr/docs/adr/001-retry.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/flows/genkovich-sdd/docs/features/cache/adr/001.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/flows/spec-kit/specs/cache/plan.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/flows/superpowers/docs/superpowers/specs/2026-07-01-cache-design.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/graph.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/hosts/claude/.claude/settings.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/hosts/claude/.mcp.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/hosts/codex/.codex/config.toml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/hosts/codex/.codex/hooks/hooks.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/bootstrap/scan/docs/adr/001-safe.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/corpus/PUBLIC_CORPORA.txt +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/corpus/openspec/_provenance.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/corpus/openspec/graph.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/corpus/self-corpus/_provenance.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/corpus/self-corpus/expected/retrieval-engine-reader.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/corpus/self-corpus/graph.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/adr/0001-use-sessions.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/bmad/ARCHITECTURE-SPINE.md +0 -0
- {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
- {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
- {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
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/genkovich/0001-queue-backpressure.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/genkovich/sad.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/genkovich/tree/docs/features/payments/adr/0001-queue-backpressure.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/genkovich/tree/docs/features/payments/sad.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/genkovich/tree/docs/features/payments/spec.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/design-empty.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/design-h3-split.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/design-with-summary.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/design.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/proposal-alternatives.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/proposal.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/changes/add-thing/design.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/changes/add-thing/proposal.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/changes/add-thing/specs/cap/spec.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/changes/add-thing/tasks.md +0 -0
- {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
- {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
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/config.yaml +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/openspec/tree/openspec/specs/cap/spec.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/spec-kit/plan.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/spec-kit/tree/specs/003-payment-retries/plan.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/spec-kit/tree/specs/003-payment-retries/spec.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/superpowers/2026-07-01-example-design.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/flows/superpowers/2026-07-01-thin-design.md +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/fixtures/mini_graph.json +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_anchoring.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_anchoring_mapping_refresh.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_apply.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_catalog.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_cli.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_integrations.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_model.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_planner.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_proof.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_review.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_bootstrap_scan.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_capture_auto_accept.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_capture_facts.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_capture_integration.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_capture_neighbors.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_capture_propose.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_capture_redact.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_compact.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_doctor.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_domains.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_import.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_init.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_okf.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_ratify.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_ratify_facts.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_stats.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_sync.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_sync_check.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_cli_viz.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_codex_plugin.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_community_baseline.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_config.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_corpus_expected_renders.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_corpus_self_corpus.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_coverage_telemetry_e2e.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_digest_integrity.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doc_corpus_integration.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doc_import.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_docs_claims.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_auto_share.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_code_drift.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_dangling_supports.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_duplicate_entity.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_graph_root_mismatch.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_never_surfaced.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_scan.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_stale_instructions.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_doctor_unratified_accept.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_domains.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_entity_duplicate_resolution.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_entity_get_or_create_race.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_env_isolation.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_fact_reachability_gate.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_flow_profile.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_git_bindings_blame.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_git_bindings_hook.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_git_bindings_provenance.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_claude_settings.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_host_session_start.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_importer.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_mind_model_additive_fields.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_okf.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_openspec_profile.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_pathless_descriptor_adoption.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_pilot_kit_corpus_fit.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_proposal_lifecycle.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_ratify_listing_gone_dark.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_ratify_policy.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_community_labels.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_doc_nodes.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_graph.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_integration.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_nodes_in_file.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_perf.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_rationale.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_read.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_resolve.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_subdir_mismatch.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_reader_version.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_redaction_seeded.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_repoint_integration.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_budget_counters.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_context.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_drift_marker.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_drilldown.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_facts.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_ids.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_integration.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_rank.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_seeds.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_shown_ids.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_terminal_evidence.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_thin_tools.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_toc.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_toptier.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_retrieval_unratified.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sandbox_hygiene.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_schema_descriptor.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_schema_domain.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_schema_fact.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_add_anchors.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_anchoring.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_capture.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_coverage_events.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_domains.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_drilldown.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_facts.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_find_entity.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_get_store.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_get_task_context.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_import.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_mcp_smoke.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_ratify_facts.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_redaction.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_render_events.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_supersede.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_sync_anchors.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_telemetry.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_thin_tools.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_server_verify.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_stats_graph.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_stats_model.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_stats_render.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_stats_skill.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_stats_snapshot.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_atomic_write_tmp_names.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_canonical_stat.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_compact.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_compact_review.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_concurrent_open.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_coverage_telemetry.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_derived_community.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_domains.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_entities.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_fact_cascade.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_facts.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_meta.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_migration.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_mutation_guard.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_mutation_immediate.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_opens_with_merged_duplicate.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_persistence.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_ratification.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_rebuild_atomicity.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_render_events.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_retrieval.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_slug_conflicts.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_stamping_marker.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_telemetry.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_threading.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_store_tmp_sweep_age.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_clean.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_domains.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_drift_cache.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_fresh_clone.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_integration.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_rebind.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_repoint.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_report_has_findings.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_run.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_toc_cache.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_volatile_heal.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_sync_wiring.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_telemetry_retention.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_verify_snapshot.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_verify_transitions.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_viz_asset_packaged.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_viz_model.py +0 -0
- {sidegraph-0.3.0 → sidegraph-0.3.1}/tests/test_viz_render_html.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
23
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
220
|
-
5. A per-session marker (`pretool_nudge:<
|
|
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.
|
|
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.
|
|
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/",
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
544
|
-
if not
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 / "
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|