Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NZM — Named Zellij Manager

NZM is a local Rust control plane for running coding agents in named Zellij sessions. It generates KDL layouts, tracks sessions and panes in SQLite, exposes machine-readable JSON, serves a REST/SSE API, and includes a Ratatui dashboard.

Requirements

Install Rust 1.85 or newer, Zellij 0.40 or newer, and whichever agent CLIs you intend to launch. NZM currently recognizes Claude Code, Codex, Gemini, and shell panes.

Build and test

cargo build --release
cargo test --workspace

The binary is written to target/release/nzm.

Basic usage

# Create a session with two Claude Code panes and one Codex pane
nzm spawn example --cc 2 --cod 1

# Inspect and manage sessions
nzm list
nzm status example
nzm attach example
nzm read example --agent cc_1
nzm kill example

# Generate a reusable layout
nzm layout generate --cc 2 --cod 1 --gmi 0

# Machine-readable state
nzm --robot-status
nzm --robot-snapshot
nzm robot capabilities

# Reconcile the registry with live zellij state (e.g. after a manual kill)
nzm sync

# Interactive dashboard and HTTP API
nzm dashboard
nzm serve --port 7337

# Run the API as a loopback-only background service (systemd user unit)
nzm service install
nzm service status
nzm service logs
nzm service uninstall

# One-shot, read-only activity evidence for registered agents (JSON)
nzm activity snapshot

Run nzm --help or nzm <command> --help for the exact interface supported by your build.

Configuration

NZM reads ~/.config/nzm/config.toml. Defaults work without a configuration file. State is stored under ~/.local/share/nzm/, and generated layouts are stored under ~/.config/nzm/layouts/.

Example:

database_path = "/home/me/.local/share/nzm/nzm.db"
layout_dir = "/home/me/.config/nzm/layouts"

[defaults]
cc = 1
cod = 0
gmi = 0

HTTP API

Start the server with nzm serve --port 7337. The current API provides health, session, agent, checkpoint, and SSE endpoints under /health, /api/v1, and /events.

For compatibility the CLI default binds all interfaces (serve --host defaults to 0.0.0.0). The installed nzm.service unit always pins loopback (--host 127.0.0.1), so an always-on background service never exposes the API to the network. If the chosen address is already in use (for example by the installed service on 127.0.0.1:7337), nzm serve reports a bind error and suggests checking nzm service status or picking another --host/--port.

Session and agent tracking

While nzm serve is running, NZM reconciles SQLite state against live Zellij sessions once per second and broadcasts committed session and agent status transitions over SSE. Pane identity remains available through the session and agent REST responses; there is no pane-level or exit-code event stream. The registry, not the event feed, is the authoritative snapshot.

Reconciliation is measured: every completed tick emits exactly one debug-level record carrying duration_ms, sessions_checked, session_changes, agent_changes, and result (ok | error | join_error); failed ticks carry zero counts because no report was committed. Passes are serialized — a tick never starts before the previous one finishes — and missed ticks are skipped rather than caught up. Because the cadence is a fixed one second, these debug records stay quiet under the default info logging; observe them with RUST_LOG=debug nzm serve, or read them from the user journal for the installed unit (see below). The one-second cadence is kept unless live measurement shows a p95 pass time above 250 ms or any pass at one second with realistic local session counts — only measured need may change the interval.

nzm sync remains available as a manual repair command when the server is not running (for example, after a Zellij session is killed directly).

Background service

nzm service manages a loopback-only systemd user unit named nzm.service that keeps nzm serve running after the launching shell exits:

  • nzm service install resolves the current executable and the zellij binary up front (failing before any write when either cannot be resolved), writes the unit to ~/.config/systemd/user/nzm.service (honoring $XDG_CONFIG_HOME), probes the current binary with serve --help to confirm it advertises --host, then runs systemctl --user daemon-reload and systemctl --user enable --now nzm.service. The generated unit is Type=simple with Restart=on-failure and RestartSec=5, executes serve --host 127.0.0.1 --port 7337, carries an explicit Environment=PATH built from the resolved directories of the current executable and zellij plus standard system directories, and is WantedBy=default.target. Installing an identical unit skips rewriting the file but still checks linger. A divergent existing unit is refused, never overwritten.
  • Without linger, systemd tears down the user manager on logout, so nzm.service stops. If linger is off (or cannot be queried), install prints loginctl enable-linger and still succeeds. nzm service install --linger runs that command for the current user; polkit denial is a warning, not an install failure. Uninstall never runs loginctl disable-linger.
  • nzm service status and nzm service logs delegate to systemctl --user status nzm.service and journalctl --user -u nzm.service (no pager), forwarding output verbatim and preserving the delegated exit status. Service logs land in the user journal, not in files; the debug-level tracking records described above are part of that journal when debug logging is enabled (systemctl --user set-environment RUST_LOG=nzm=debug, then restart the unit).
  • nzm service uninstall stops and disables the unit with systemctl --user disable --now, removes only the unit file, and reloads user systemd; a missing unit file is tolerated. The registry database, generated layouts, and Zellij sessions are never touched, so uninstalling is reversible with another install. Linger is left as the operator set it.

The service runs without root privileges and only reaches 127.0.0.1:7337.

Activity snapshot

nzm activity snapshot is one-shot, read-only activity evidence for registered agents, designed for external consumers:

  • Stateless and pure: nothing is stored, no agent status is mutated, and no screen text is parsed. NZM reports only what it observed; interpretations such as quiet or idle are left to the consumer.
  • For every registered agent with a bound pane, the exact bytes that zellij's dump-screen emits are hashed with deterministic FNV-1a (64-bit, standard offset basis and prime) and reported as 16 lowercase hex digits. The hash is defined over the raw bytes — no UTF-8 lossy conversion — so an identical screen always yields an identical hash.
  • A missing bound pane or a failed dump is reported with probe=unknown, signal=probe_failed, a null screen_hash, and a detail string. Agents the registry marks Stopped or Error are reported with probe=unknown, signal=lifecycle, and a null screen_hash: registry lifecycle truth overrides screen probing entirely.
  • Output is exactly one JSON document on stdout and exit status 0, even for zero registered agents. Hard failures (registry open/query, serialization) exit nonzero with empty stdout; diagnostics go to stderr. The schema is frozen as version 1: the envelope carries version, generated_at, and an agents array; each agent carries subject (the stable join key <session>:<label> — pane ids never enter subjects), session, agent_label, agent_type, probe (observed | unknown), signal (screen_observed | lifecycle | probe_failed), observed_at, screen_hash (or null), zellij_pane_id (or null), lifecycle (running | stopped | error), and detail (or null). Frozen consumers reject unknown fields and values outside the documented enums.

notifier-ng integration

notifier-ng consumes NZM evidence through its own adapter and a managed user timer; NZM itself never delivers notifications:

  • The adapter invokes nzm robot snapshot for lifecycle and nzm activity snapshot for evidence, validates both against the frozen contracts, joins subjects by <session>:<label>, and classifies each agent subject: sessions NZM reports stopped map to stopped; agents reported error map to error (needs attention); observed activity maps to active; screen evidence unchanged across the quiet threshold maps to idle notifications labeled quiet_only; unknown/probe_failed evidence emits no record — a failed probe never fabricates a notification. While a notifiable state persists (quiet past the threshold, stopped, error) the evidence is re-emitted on each run; delivery deduplication belongs to notifier core, so a repeated quiet never delivers twice until the screen changes — any change rearms the adapter.
  • The quiet threshold is adapter policy, not NZM policy: NZM_QUIET_SECONDS (default 900) in the adapter's environment, with hysteresis state under notifier-ng's XDG state directory. First observation baselines without delivery.
  • Codex, Hermes, and OMP hooks remain the authoritative idle signal; NZM-backed quiet is a conservative complement, never a replacement — quiet means no observable screen change, not proof of waiting.
  • The adapter runs on a one-minute user timer managed by the notifier-ng installer (dry-run/--apply, never clobbering existing units), which prints the exact systemctl --user enable commands rather than invoking systemctl itself. Cutover disables the legacy notifier-ng-zellij.timer only after the NZM-backed timer has been verified from the journal; rollback re-enables that timer and removes the NZM timer units. nzm service uninstall remains safe at any point: it touches only the NZM unit.

Development

The public CI runs formatting, Clippy with warnings denied, and workspace tests on GitHub-hosted ubuntu-latest runners. See CONTRIBUTING.md and SECURITY.md.

License

GNU Affero General Public License v3.0 or later. See LICENSE.

About

Named Zellij Manager for multi-agent development sessions

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages