Context
Smriti's first long-running process. Captures sessions across all configured coding agents (Claude, Codex, Cline, Cursor, Copilot) so users don't have to remember which agent they used or to run `smriti ingest ` manually.
Full design and rationale in `docs/internal/daemon-prd.md` — that document is the source of truth; this issue tracks the work.
Supersedes #7 and #70.
Scope
A single-process daemon, started automatically at user login on macOS and Linux, that:
- Watches all configured agent log roots via `fs.watch` (macOS/Windows recursive natively, Linux walks the tree)
- Debounces per-project for 30s, then runs the existing ingest pipeline (`src/ingest/`) in-process
- Accepts a poke from the Claude Stop hook over a Unix socket as an additional signal
- Survives crashes (re-triggered by next FS event) and reboots (via LaunchAgent/systemd-user)
- Writes to the same SQLite the CLI reads from — no protocol, no IPC for reads
The daemon is parse-and-write only. The existing CLI surface (`smriti search`, `smriti recall`, `smriti share`, `smriti embed`) keeps working unchanged.
Tasks
Daemon core (`src/daemon/`)
Service installer (`src/daemon/install.ts`)
CLI subcommands (in `src/index.ts`)
Hook update
Tests
Docs
Acceptance criteria
From the PRD:
- After `smriti daemon install`, the daemon survives a logout/login cycle and a full reboot without user intervention.
- Opening Cursor on a new project, doing some work, and never running a smriti command — that project's sessions appear in `smriti search` within `30s + ingest_time` of the work being saved.
- Claude Code's Stop hook completes within 50 milliseconds when the daemon is running.
- SIGKILLing the daemon leaves no stale socket, no stale PID file, no corrupt SQLite state. Re-running it works cleanly.
- Running `smriti daemon install` twice produces idempotent results — same plist/service file, same registered job, no duplicates.
- `smriti share` continues to work, unchanged, with its existing sanitization. No new redaction error paths.
- Removing the daemon (`smriti daemon uninstall`) leaves the system in exactly the state it was before installation — no orphaned files, no lingering processes.
Out of scope (deferred to later phases)
Enumerated here so the boundary is unambiguous:
- No redaction pipeline yet. `smriti share` keeps its current sanitization. A real redaction pipeline is the next phase after this.
- No read-side routing. `smriti search` / `recall` remain one-shot CLI invocations — ~150ms cold-start is tolerable, not worth daemon-routing complexity.
- No QMD MCP daemon coordination. Both daemons can run concurrently; SQLite WAL handles it.
- No QMD upstream proposal (`searchFTS({ joins })` is orthogonal).
- No Windows. macOS and Linux only.
- No embedding model in the daemon. `qmd embed` stays the way embeddings happen — adding it to the daemon would duplicate QMD's model load and add ~300–500 MB resident for no gain.
Design notes
A summary of the reasoning behind the choices above, for future maintainers landing on this issue.
Why a daemon at all?
The lockf mitigation in `~/.claude/hooks/save-memory.sh` solves the Claude pile-up problem. But it only covers Claude — Codex CLI, Cursor, Cline, and Copilot have no equivalent Stop hook. Their session logs sit on disk and only enter Smriti when the user remembers to run `smriti ingest `. The actual product gap is "I switch coding agents during the week and my memory has holes." A filesystem watcher closes that gap; nothing simpler does.
Why auto-start at login and not a foreground `smriti watch` command?
A foreground command means users have to remember to run it after every reboot. The point of capture is that it happens whether the user thinks about it or not. The cost is per-OS service-file generators (LaunchAgent for macOS, systemd-user for Linux) — meaningful engineering but bounded, and a one-time install step.
Why not capture at commit time (Entire-style)?
Considered seriously. Entire CLI uses git hooks (`post-commit`) to capture sessions, which dodges the daemon problem entirely. The trade-off: commit-time capture loses pre-commit exploratory work (the rabbit holes that don't end in a commit). For Smriti's personal-recall use case ("what did I try yesterday that didn't work?") that exploratory work is the point. Session-boundary capture preserves it; commit-boundary doesn't. We may add a commit-hook layer later as an additional signal, but session-level capture stays primary.
Why no embedding work in the daemon?
The embedding model is ~300–500 MB resident and takes 2–5 seconds to load. QMD already owns embedding via `qmd embed` (one-shot batch CLI). Putting embedding in the daemon would either duplicate the model (if `qmd mcp --daemon` is also running) or fight for ownership of it. Neither is worth it for phase 1, when BM25 search works perfectly well against newly-ingested content. Vectors lag ingest by however long until the next `qmd embed` run — fine.
Why ship without a real redaction pipeline?
Phase 1 is local-only. Nothing in this phase makes captured content leave the user's machine. The existing `smriti share` already does basic sanitization (XML noise, interrupt markers, API errors). That's sufficient for the current curated-knowledge sharing flow. A proper redaction pipeline (high-entropy strings, credentialed URIs, vendor secret patterns, typed placeholders) becomes load-bearing the moment Smriti starts handling raw transcripts at scale — but that's the next phase, not this one.
Why per-project debounce and not global?
A busy session in project A shouldn't delay project B's ingest. Per-project keeps the latency floor at `DEBOUNCE_MS` regardless of how many projects are active. The cost is one timer per active project, which is trivial.
Why a Unix socket for single-instance, not a PID file?
The socket file is kernel-cleaned on process exit — no stale-recovery code. The same socket doubles as the IPC transport for the Claude Stop hook poke. One mechanism, two jobs. (We still write a diagnostic PID file at `~/.cache/smriti/daemon.pid` for `smriti daemon status` and human grep, but it's not load-bearing for mutual exclusion.)
Trajectory
- Phase 1 (this issue) — cross-agent daemon, auto-start, macOS + Linux.
- Phase 1.5 — real redaction pipeline. Re-shapes `smriti share` to handle raw transcripts safely alongside the curated knowledge it already produces.
Beyond that, the trajectory tracks what users actually ask for — additional agent integrations, search quality improvements as QMD evolves, and ergonomics around the team-sharing flow.
Context
Smriti's first long-running process. Captures sessions across all configured coding agents (Claude, Codex, Cline, Cursor, Copilot) so users don't have to remember which agent they used or to run `smriti ingest ` manually.
Full design and rationale in `docs/internal/daemon-prd.md` — that document is the source of truth; this issue tracks the work.
Supersedes #7 and #70.
Scope
A single-process daemon, started automatically at user login on macOS and Linux, that:
The daemon is parse-and-write only. The existing CLI surface (`smriti search`, `smriti recall`, `smriti share`, `smriti embed`) keeps working unchanged.
Tasks
Daemon core (`src/daemon/`)
/.cache/smriti/daemon.sock`), single-instance guard via bind failure, signal handling (SIGTERM/SIGINT graceful), diagnostic PID file at `/.cache/smriti/daemon.pid`Service installer (`src/daemon/install.ts`)
CLI subcommands (in `src/index.ts`)
Hook update
```bash
#!/bin/bash
SOCK="$HOME/.cache/smriti/daemon.sock"
if [ -S "$SOCK" ]; then
: | nc -U "$SOCK" 2>/dev/null
else
/usr/bin/lockf -t 0 /tmp/smriti-ingest.lock smriti ingest claude 2>/dev/null
fi
exit 0
```
Tests
Docs
Acceptance criteria
From the PRD:
Out of scope (deferred to later phases)
Enumerated here so the boundary is unambiguous:
Design notes
A summary of the reasoning behind the choices above, for future maintainers landing on this issue.
Why a daemon at all?
The lockf mitigation in `~/.claude/hooks/save-memory.sh` solves the Claude pile-up problem. But it only covers Claude — Codex CLI, Cursor, Cline, and Copilot have no equivalent Stop hook. Their session logs sit on disk and only enter Smriti when the user remembers to run `smriti ingest `. The actual product gap is "I switch coding agents during the week and my memory has holes." A filesystem watcher closes that gap; nothing simpler does.
Why auto-start at login and not a foreground `smriti watch` command?
A foreground command means users have to remember to run it after every reboot. The point of capture is that it happens whether the user thinks about it or not. The cost is per-OS service-file generators (LaunchAgent for macOS, systemd-user for Linux) — meaningful engineering but bounded, and a one-time install step.
Why not capture at commit time (Entire-style)?
Considered seriously. Entire CLI uses git hooks (`post-commit`) to capture sessions, which dodges the daemon problem entirely. The trade-off: commit-time capture loses pre-commit exploratory work (the rabbit holes that don't end in a commit). For Smriti's personal-recall use case ("what did I try yesterday that didn't work?") that exploratory work is the point. Session-boundary capture preserves it; commit-boundary doesn't. We may add a commit-hook layer later as an additional signal, but session-level capture stays primary.
Why no embedding work in the daemon?
The embedding model is ~300–500 MB resident and takes 2–5 seconds to load. QMD already owns embedding via `qmd embed` (one-shot batch CLI). Putting embedding in the daemon would either duplicate the model (if `qmd mcp --daemon` is also running) or fight for ownership of it. Neither is worth it for phase 1, when BM25 search works perfectly well against newly-ingested content. Vectors lag ingest by however long until the next `qmd embed` run — fine.
Why ship without a real redaction pipeline?
Phase 1 is local-only. Nothing in this phase makes captured content leave the user's machine. The existing `smriti share` already does basic sanitization (XML noise, interrupt markers, API errors). That's sufficient for the current curated-knowledge sharing flow. A proper redaction pipeline (high-entropy strings, credentialed URIs, vendor secret patterns, typed placeholders) becomes load-bearing the moment Smriti starts handling raw transcripts at scale — but that's the next phase, not this one.
Why per-project debounce and not global?
A busy session in project A shouldn't delay project B's ingest. Per-project keeps the latency floor at `DEBOUNCE_MS` regardless of how many projects are active. The cost is one timer per active project, which is trivial.
Why a Unix socket for single-instance, not a PID file?
The socket file is kernel-cleaned on process exit — no stale-recovery code. The same socket doubles as the IPC transport for the Claude Stop hook poke. One mechanism, two jobs. (We still write a diagnostic PID file at `~/.cache/smriti/daemon.pid` for `smriti daemon status` and human grep, but it's not load-bearing for mutual exclusion.)
Trajectory
Beyond that, the trajectory tracks what users actually ask for — additional agent integrations, search quality improvements as QMD evolves, and ergonomics around the team-sharing flow.