Expand description
Resident (long-lived) script handlers.
See src/scripting/CLAUDE.md for the full model. In short: the default
script path (super::executor) spawns a fresh interpreter per event,
so a script can keep no state between events and pays interpreter start-up
(and, for Go, a full compile) every time. A resident script is spawned
once per scope and then driven with one event per line on its stdin,
replying with one JSON line of actions per event. Because the process stays
alive, module-level variables persist across events — counters, a parsed
config, a connection map — with no re-reading.
Resident mode is opt-in ("resident": true on a script event handler);
the stateless per-event path is unchanged and remains the default.
§Trust boundary
Resident scripts run under the same unsandboxed trust boundary as
per-event scripts (see the banner in src/scripting/CLAUDE.md): the
interpreter runs as the netget user with full privileges. Resident mode adds
no new capability to the script itself — it only keeps the same interpreter
process alive between events instead of respawning it. The one operational
difference worth stating: a resident process outlives a single event, so it
is cleaned up on scope shutdown / idle eviction rather than by process exit.
§Wire protocol (newline-delimited JSON, one round-trip per event)
parent ──► {"event_type_id": "...", "server": {...}, "connection": {...}, "event": {...}}\n
child ──► {"actions": [ {...}, ... ]}\nThe child harness (built here per language) wraps the user’s code, which must
define a handle(event_type, event, message) function. The harness reads a
line, calls handle, and writes exactly one JSON line back. A user handle
that raises emits {"error": "..."}, which the parent treats as a failed
event (→ caller falls back to the LLM) — distinct from a legitimate empty
{"actions": []}.
§Robustness
Every event round-trip runs under a tokio::time::timeout. If the resident
wedges on one event or its process has died (EOF on stdout), the round-trip
returns Err, the process is killed and evicted from the registry, and the
caller falls back to the LLM. The next event for that scope spawns a fresh
process. Nothing parks an OS thread: the child is spawned with
tokio::process and awaited asynchronously.
Structs§
- Resident
Script - A single resident process. Access to its pipes is serialized by the
iomutex — one event round-trip at a time per process, which is required (two concurrent writers on one stdin would interleave) and mirrors the per-connection state machine used elsewhere. - Resident
Script Manager - Manager for resident scripts. All methods are static; state lives in the
process-global [
REGISTRY].
Enums§
- Resident
Scope - Scope that decides which events share one resident process (and therefore
share in-process state). Mirrors the scheduled-task scopes in the root
CLAUDE.md.
Functions§
- resident_
available - Whether the interpreter for a resident language is installed. Mirrors the per-event executor’s availability check so a resident handler for a missing interpreter falls back to the LLM rather than erroring per event.
- resident_
language_ supported - Is this language supported in resident mode?