README.mdpreviewREADME.mdsource143 lines · 6.3 KB · raw
1# Chapter 11: jevhooks-daemon, one process for the machine
2
3You might have three Claude Code sessions open, each about to run a
4command. Each could open its own connection to Jev, fetch the key, and keep
5its own log. Instead they all talk to **one daemon**: a small local server,
6one for the whole machine, listening on a Unix socket. It opens the
7connection to Jev once and keeps it warm, holds the key in one process and
8nowhere else, and writes every decision from every session to one log.
9
10This crate builds one binary, `jevhooks`, that plays every part:
11
12| Command | What it is |
13| --- | --- |
14| `jevhooks serve` | The daemon. |
15| `jevhooks mcp` | The MCP server a Claude Code session starts (the plugin's `.mcp.json`). |
16| `jevhooks status` | Prints the running daemon's status. |
17| `jevhooks recent [count]` | Prints the latest decisions and outcomes, oldest first (20 by default). |
18| `jevhooks stop` | Asks the daemon to stop. |
19
20## Who starts the daemon
21
22Nobody starts it by hand. Each session starts `jevhooks mcp`, and the MCP
23server's first act is to make sure a daemon is running, and that it is the
24right one:
25
26```mermaid
27sequenceDiagram
28  participant S as Claude Code session
29  participant M as jevhooks mcp
30  participant D as jevhooks serve
31  S->>M: start (stdio)
32  M-->>S: two tools, at once
33  M->>D: GET /status
34  alt same build answers
35    D-->>M: identity matches: done
36  else another build answers
37    M->>D: POST /shutdown, wait for the socket to close
38    M->>D: start one from this executable
39  else nothing answers
40    M->>D: start one from this executable
41  end
42  M->>D: GET /status until it answers (up to 30 s)
43```
44
45"The right one" means **the same build**. A daemon's identity is its
46executable's device, inode, size and modification time, so the first
47session after `just daemon` sees a different identity, retires the old
48daemon and starts the new one. You never have to remember to restart it.
49
50The MCP server does not wait for any of that before answering Claude Code:
51fetching the key can take seconds, and a session should not wait on that to
52list two tools. The two tools are `status` and `recent`, both read-only,
53both just the daemon's own routes.
54
55> **Aside: a socket, a lock, and a process group.** Three small things
56> keep "one daemon" true. The socket is a file in the state directory, so
57> only processes on this machine, and with that user's permissions, can
58> reach it. A lock file (`daemon.lock`) is held for as long as a daemon
59> runs, so a second one started by accident sees it, says so on stderr,
60> and exits. And the daemon is started in its own process group, so the
61> session that started it can end without taking it along.
62
63## The key
64
65`TYPESAFE_API_KEY` reaches the daemon one of two ways: it is already in the
66environment of the session that starts it, and is inherited; or it is not,
67`op-env-run` is on `PATH`, and the daemon is started through it, which puts
68the key in the daemon's environment and nowhere else. With neither, the
69daemon still runs, asks Jev nothing, and every verdict is "pass"; `status`
70says `no key`.
71
72## What it keeps
73
74All under `$XDG_STATE_HOME/jevhooks` (`~/.local/state/jevhooks` when that is
75not set):
76
77| File | What |
78| --- | --- |
79| `daemon.sock` | The socket every session's mod and MCP server talk to. |
80| `daemon.lock` | Held while a daemon runs, so two cannot serve at once. |
81| `daemon.log` | The daemon's stderr. |
82| `decisions.jsonl` | Every judgment (what was judged, every option's probability, the verdict, time and cost) and what became of each judged command (`ran`, `failed`, `denied`). This is the evidence for tuning the thresholds. |
83
84Spend is booked in Jev's shared ledger, kept by `jev-http` under
85`$XDG_STATE_HOME/jev`, whose guards apply across every program on the
86machine that asks Jev: 30 questions a minute and $0.25 an hour by default
87(`JEV_MAX_QUESTIONS_PER_MINUTE`, `JEV_MAX_DOLLARS_PER_HOUR`). A question the
88ledger refuses is a "pass".
89
90## Its one setting
91
92`$XDG_CONFIG_HOME/jevhooks/config.toml` (`~/.config/jevhooks/config.toml`).
93The file is optional, and so is its one key:
94
95```toml
96# The headroom gate asks before a heavy command when too little memory is
97# available. By default "available" is the kernel's own figure (MemAvailable
98# on Linux), which is right on bare metal. Set a command where that figure is
99# wrong or missing: inside a virtual machine that cannot see its host, or on a
100# system with no such figure. It must print the megabytes available as its
101# first number, within two seconds.
102available_memory_command = "my-host-memory --megabytes"
103```
104
105A misspelt key is reported by `status`, and the daemon runs on the defaults:
106a typo must not switch the gate off. Where there is neither a built-in
107figure nor a command, a command's load is simply not judged; everything
108else works.
109
110> **Try it.** Build it (`just daemon`, which needs the rmcp fork) and start
111> a session once, then from the repository root:
112>
113>     plugin/bin/jevhooks status
114>     plugin/bin/jevhooks recent 5
115>
116> `status` shows whether Jev can be asked (`ready`, `no key` or `broken`),
117> the pinned model, how many events it has decided and observed, the spend
118> ledger's line, where the memory figure comes from and what it reads now,
119> and every threshold that turns a probability into a verdict.
120
121## For the people who maintain it
122
123Measured on Claude Code 2.1.287: the mod's call to the daemon adds 4 to 13
124ms to the daemon's own 120 to 260 ms; the MCP server connects in 55 ms.
125
126The daemon's routes, served with hyper as HTTP/1.1 on the socket:
127
128| Route | What |
129| --- | --- |
130| `POST /decide` | A deciding event; answers with a `Decision`. |
131| `POST /observe` | An event the daemon needs but does not answer. |
132| `GET /status` | Whether Jev can be asked, the identity, counts, the ledger, the headroom source and the thresholds. |
133| `GET /recent?count=20` | The last records of `decisions.jsonl`, oldest first. |
134| `POST /shutdown` | Stop, so a newer build can take the socket. |
135
136### In this folder
137
138| Path | What |
139| --- | --- |
140| [src/](src/) | Chapter 12: the questions, the verdicts, and the daemon's parts. |
141| [Cargo.toml](Cargo.toml) | The `jevhooks` binary: hyper, tokio, rmcp, clap, toml, and `jev-http`/`jev-protocol` from `third-party/jevcrates`. |
142
143← Previous: [Chapter 10, jevhooks-mod/src/](../jevhooks-mod/src/) · Up: [crates](../) · Next: [Chapter 12, jevhooks-daemon/src/](src/) →