Skip to main content

Module resident

Module resident 

Source
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": [ {...}, ... ]}\n

The 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§

ResidentScript
A single resident process. Access to its pipes is serialized by the io mutex — 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.
ResidentScriptManager
Manager for resident scripts. All methods are static; state lives in the process-global [REGISTRY].

Enums§

ResidentScope
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?