Skip to content

feat(install): wire SessionStart daemon autostart in ctx-install (close pip-install gap)#10

Open
hang-in wants to merge 1 commit into
jaytoone:masterfrom
hang-in:feat/upstream-ctx-install-sessionstart-wiring
Open

feat(install): wire SessionStart daemon autostart in ctx-install (close pip-install gap)#10
hang-in wants to merge 1 commit into
jaytoone:masterfrom
hang-in:feat/upstream-ctx-install-sessionstart-wiring

Conversation

@hang-in
Copy link
Copy Markdown
Contributor

@hang-in hang-in commented May 16, 2026

Summary

pip install ctx-retriever && ctx-install is documented as matching the plugin Setup path for daemon autostart (README L88-90: "bge-daemon starts automatically on session open"), but in practice ctx-install was never wiring a SessionStart hook. Daemons did not start on session open for pip-based installs; affected users sat permanently on vec_daemon_down in telemetry.

This PR ships the missing SessionStart wiring so the two install paths produce equivalent results.

Background

The plugin Setup path's plugin/hooks/hooks.json already has a SessionStart entry that probes each daemon socket and respawns if dead — done via a ~700-char inline bash block embedded in JSON. That works for the marketplace path because the JSON is generated.

For ctx-install-managed settings.json, the same logic is easier to audit / version / fix as a maintained script. (One of us already had to land a stale-socket hardening fix in a script-form of this hook locally; that pattern is now in the upstream codebase.)

Changes

File Δ Purpose
src/hooks/ensure-claude-vault-daemons.sh +56 (new) Maintained autostart script — pipx → venv → graceful exit interpreter detection; pgrep as liveness source of truth; stale sock+pid cleanup before respawn
src/cli/install.py ±28 CTX_HOOKS adds SessionStart row; _hook_entry branches on .sh vs .py extension (bash vs python3)
pyproject.toml ±2 [tool.setuptools.package-data] adds *.sh so script ships in the wheel

Design choices

Why a script, not an inline command (like the plugin Setup path)

  • Auditable: 60 lines of commented bash beats a 700-char single-line JSON embed
  • Versioned: future fixes (stale-socket handling already needed once) live in git
  • Two-path equivalence is preserved: both paths probe sockets and respawn

Why pgrep as liveness source of truth

A *.sock file alone is not a reliable liveness signal — daemon SIGKILL'd / system reboot / terminal close leaves the socket undeleted. The previous [ ! -S sock ] && ! pgrep short-circuits on the stale socket and skips spawn forever. pgrep is the only signal that's actually re-derived per check; on dead, we unlink the stale sock+pid and respawn.

Interpreter detection priority

# Path When
1 ~/.local/pipx/venvs/ctx-retriever/bin/python pipx install ctx-retriever (default per README Option A)
2 ~/.local/share/claude-vault/venv/bin/python Plugin Setup path (per plugin/hooks/hooks.json)
3 (graceful exit) Neither available — BM25-only mode

sentence_transformers must be importable in the chosen interpreter — otherwise we fall through (no spawn, exit 0).

bge-daemon policy

Kept the plugin Setup path's CTX_BGE_ENABLE=1 opt-in (semantic rerank loads a 58s model on first run) so behaviour matches across install paths.

Validation

  • bash -n src/hooks/ensure-claude-vault-daemons.sh — syntax OK
  • _new_hooks_block() unit-verified to emit:
    "SessionStart": [{ "hooks": [{ "type": "command",
      "command": "bash $HOME/.claude/hooks/ensure-claude-vault-daemons.sh" }]}]
  • Live test against a fork-local environment where both daemons had been silently down for 6 days: a single invocation of the new script restarted both cleanly, logs streaming as expected. Stale *.sock + *.pid cleaned automatically.

Backwards compat

  • ctx-install is idempotent: re-running on an already-wired settings.json is a no-op (existing dedupes by command string logic in settings_patcher handles the new SessionStart entry the same way)
  • --uninstall removes the new SessionStart entry via the same mechanism (no extra changes needed)
  • Existing users who manually wired a custom autostart script: their entry survives because it's a different command string

Why this is a maintainer-area change

Per MAINTAINERS.md the install machinery + hook hardening areas land with the second maintainer. Flagging this here so it's clear this isn't algorithm or benchmark territory.

Related: README L88-90 (daemon-autostart promise), plugin/hooks/hooks.json SessionStart inline (the equivalent for the marketplace path).

…gap)

`pip install ctx-retriever && ctx-install` was advertised as matching the
plugin Setup path's daemon-autostart behaviour, but in practice
`ctx-install` never wired a SessionStart hook. Daemons therefore did not
start on session open for pip-based installs, despite README L88-90
("bge-daemon starts automatically on session open"). Telemetry stayed
permanently on `vec_daemon_down` for affected users.

The plugin Setup path uses an inline bash block inside
`plugin/hooks/hooks.json` to probe + spawn each daemon. This PR ships
the same logic as a maintained script so the two install paths produce
equivalent results.

## Changes

1. **`src/hooks/ensure-claude-vault-daemons.sh`** (new, +56 lines)
   - Detects Python interpreter in priority order: pipx env → claude-vault
     venv → graceful exit (BM25-only mode)
   - `pgrep` is source of truth for liveness; stale `*.sock` / `*.pid`
     get cleaned before respawn (fixes the silent-skip-on-dead-daemon
     failure mode where a leftover socket file blocked any restart)
   - `bge-daemon` opt-in via `CTX_BGE_ENABLE=1` (matches the plugin
     SessionStart inline block)

2. **`src/cli/install.py`**
   - `CTX_HOOKS` adds `(ensure-claude-vault-daemons.sh, SessionStart, async)`
   - `_hook_entry` now branches on extension: `.sh` files run via `bash`,
     `.py` files via `python3`
   - Hook copy step already handles `.sh` (chmod 0o755 was already set for
     all hook files); no additional changes needed there

3. **`pyproject.toml`**
   - `[tool.setuptools.package-data]` for `ctx_retriever.hooks` adds
     `*.sh` so the script ships in the wheel

## Why a script instead of an inline command

The plugin Setup path uses a ~700-char inline bash block embedded in JSON.
That works for the marketplace path because the JSON is generated; for
`ctx-install`-managed `settings.json` a maintained script is easier to
audit, fix, and version (one of the maintainers landed a stale-socket
hardening fix to a script-form of this hook recently — that pattern is
encoded here verbatim).

## Validation

- Bash syntax: `bash -n src/hooks/ensure-claude-vault-daemons.sh` OK
- `_new_hooks_block()` output now includes `SessionStart` group with the
  expected `bash $HOME/.claude/hooks/...` command (unit-verified)
- pipx-env detection verified in a fork-local environment where daemons
  had been silently down for 6 days — script restarted both daemons
  cleanly on first invocation after fix

Refs: ensure-claude-vault-daemons.sh stale-socket discussion (local
maintainer notes); MAINTAINERS.md areas-of-ownership (install machinery
+ hook hardening).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant