Start the daemon from whichever hook fires first

A plugin's SessionStart hook never runs, so nothing was starting the
daemon: no daemon.log, nothing on :8791, and a manual `glance up`
working perfectly.

Grok Build dispatches SessionStart from inside session creation
(xai-grok-shell, agent_ops.rs -> DispatchSessionStartHook) and resolves
it against the session's hook registry as it stands at that moment.
That registry comes from discover_hooks(), whose sources are the config
layers and the global/project settings files; plugin directories are not
among them. Plugin hooks are appended later, under a plugin/ prefix, by
reload_hooks_impl and reload_plugins_impl - which run in response to a
plugin action, a /hooks reload, or a folder-trust grant. So the entry is
always registered after the event it subscribes to has been dispatched.
The other thirteen events work because they happen later in the session.

There is no boot event to move to, so every recorder boots the daemon
instead and whichever fires first wins. The cost is one loopback request
to /healthz per event once it is up, which is the steady state. A
daemon.lock (O_EXCL, 15s staleness takeover) keeps a burst of concurrent
events from starting five daemons and leaving four to die on EADDRINUSE.

glance-up.mjs stays wired: it costs nothing when it does not fire, and
it is the right hook for the job if that ordering is ever fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iceBear67
2026-08-09 08:04:03 +00:00
co-authored by Claude Opus 5
parent b586323fdb
commit 966133eeda
7 changed files with 201 additions and 70 deletions
+30 -3
View File
@@ -53,8 +53,9 @@ git clone <this repo> ~/.grok/plugins/grok-glance
```
That is the whole thing — no `npm install`, no build. Open `/plugins` in Grok Build and enable
**grok-glance**. On the next session start its `SessionStart` hook boots the daemon in the
background, and the dashboard is on `http://127.0.0.1:8791`.
**grok-glance**. The first hook to fire after that — your next prompt, or the first tool call —
starts the daemon in the background, and the dashboard is on `http://127.0.0.1:8791`. (Not the
`SessionStart` hook, which for a plugin never runs; see [Hook wiring](#hook-wiring).)
### …or from a marketplace, by URL
@@ -244,6 +245,7 @@ Everything lives in `~/.grok/glance` (mode 0700), or `$GLANCE_HOME` if you set i
| `events.jsonl` | Append-only event log, one JSON object per line, rotated at 5 MB |
| `sessions.json` | The agent roster — label, badge, workspace, state, counts — so a restart comes back with the overview intact. Written debounced, flushed on shutdown; sessions older than 12 hours are dropped on load. |
| `daemon.log` | Daemon stdout/stderr |
| `daemon.lock` | Held while a hook script is starting the daemon, so a burst of events starts one and not five. Created with `O_EXCL`, deleted on the way out, and ignored by anyone else once 15s stale. |
Three environment variables override `config.json`, which is mostly useful for testing a second
instance without touching your real one:
@@ -343,7 +345,8 @@ traverses the tunnel, and it can present the shared secret — hook traffic goes
`http://127.0.0.1:8791` and never leaves the machine. So each observed event runs
`bin/glance-record.mjs`, which costs a Node start (~40 ms) and POSTs one event. Two entries differ:
- `SessionStart` runs `bin/glance-up.mjs`, which is what boots the daemon.
- `SessionStart` runs `bin/glance-up.mjs`. It is the obvious hook to boot the daemon from, and it
never runs — see below.
- `PreToolUse` is wired **twice** — a recording entry for the timeline, and a second entry matching
only `^(Bash|Write|Edit|MultiEdit|NotebookEdit)$` that runs `bin/glance-approve.mjs`. PreToolUse
is the only blocking event, and a command hook is the only documented way to return a deny
@@ -354,6 +357,30 @@ traverses the tunnel, and it can present the shared secret — hook traffic goes
The hook scripts use nothing but the Node standard library and always exit 0 unless they are
deliberately denying — including when the daemon rejects their token.
### Why every recorder boots the daemon
A plugin gets no usable boot event, so `bin/glance-record.mjs` starts the daemon itself when it
finds it missing, and whichever event fires first wins.
`SessionStart` looks like the right answer and cannot work. Grok Build dispatches it from inside
session creation (`xai-grok-shell`, `agent_ops.rs``SessionCommand::DispatchSessionStartHook`),
and the dispatch resolves against the session's hook registry **as it stands at that moment**. That
registry comes from `discover_hooks()`, whose sources are the config layers and the global/project
settings files — `~/.grok/settings.json`, `<git_root>/.grok/hooks`, the vendor-compat paths. Plugin
directories are not among them. Plugin hooks are appended separately, under a `plugin/` prefix, by
`reload_hooks_impl` and `reload_plugins_impl` — both of which run later, in response to a plugin
action, a `/hooks reload`, or a folder-trust grant. So a plugin's `SessionStart` entry is always
registered after `SessionStart` has already been dispatched, and is never called. Every other event
this plugin subscribes to happens later in the session, once the plugin registry has landed, which
is why they all work.
The symptom, if you hit this from the other end: no `daemon.log` at all, nothing on `:8791`, and a
manual `glance up` working perfectly.
Asking costs one loopback request to `/healthz` per event, which is the steady state once the
daemon is up. The spawn path is taken once. A `daemon.lock` (`O_EXCL`, 15s staleness) keeps a burst
of concurrent events from starting five daemons and leaving four of them to die on `EADDRINUSE`.
## Deliberately omitted
Not oversights — decisions: