unifier
Filesystem postbox for inter-process communication. Programs share state by writing values to files in a Unix tree, treating the filesystem as a global data structure — the same strategy used by filesystem-git-issues (directories are namespaces, files are keys, UUID-named files are messages).
No central server is required.
State layout
Default root: ~/.local/unifier (override with $UNIFIER_HOME or --home).
~/.local/unifier/
keys/<path> # persistent key-value (global)
mailbox/<recipient>/<uuid>.txt # point-to-point messages
cron/<min>_<hour>_<dom>_<mon>_<dow>/<uuid>.txt # scheduled drops
sqlite/<name>.sqlite # managed SQLite databases
sqlite/triples.sqlite # reserved subject/predicate/object store
events/<uuid>.json # JSON events (payload + created_at + expires_at)
logs/<uuid>.json # span-tree log entries
.daemon/www/<name> # temp files served over HTTP
.daemon/http.port # bound localhost web port
chroots/
<name>/
keys/
mailbox/
cron/
sqlite/ # isolated subtree (--chroot)
Cron schedule directories use five underscore-separated fields. Use * for “any” (e.g. 0_9_*_*_* = 09:00 daily).
CLI reference
| Command | Description |
|---|---|
put <key> <value> |
Write a persistent key under keys/ |
get <key> |
Read a key |
del <key> |
Delete a key |
send [--from <agent>] <recipient> <message> |
Drop a mailbox envelope (id, from, to, payload) |
message --from <agent> <recipient> '<json>' |
Structured agent mailbox message + event-socket wakeup |
event '<json>' [--ttl <secs>] |
Post a JSON event under events/ (default TTL 24h; 0 = never) |
daemon watch |
Print wakeup notices from .daemon/events.sock (for Jan cron) |
cron <schedule> <message> |
Schedule a cron message |
poll <recipient> [--ack] |
Collect mailbox messages |
poll-cron [--ack] |
Collect messages whose schedule matches now |
list <path> |
List message files under a subtree |
ack <uuid|path> |
Remove a processed message |
root |
Print the effective state root |
daemon start |
Start hot in-memory daemon (background) |
daemon stop |
Stop daemon (flushes dirty state) |
daemon gc [--dry-run] |
Kill orphan daemons whose --home is missing |
daemon status |
Show whether daemon is running |
daemon flush |
Write dirty in-memory state to disk |
tick start [label] |
Begin ACID tick (reads frozen previous state) |
tick end |
Commit tick to disk with versioning under ticks/<n>/ |
tick status |
Show committed/active tick, queue, and locks |
tick lock <key> / tick unlock <key> |
Lock keys during active tick |
chroot init <name> |
Create chroots/<name>/ with keys/, mailbox/, cron/ |
chroot list |
List chroot names |
namespace set <name> |
Bind a key prefix to this process (and descendants) |
namespace get |
Print the effective namespace |
namespace clear |
Remove this process's namespace binding |
sql list |
List managed SQLite databases under sqlite/ |
sql create <name> |
Create sqlite/<name>.sqlite |
sql tables <db> [--schema] |
List tables (and columns) in a database |
sql exec <db> '<sql>' |
Run SQL (creates the DB file if missing) |
triple add <s> <p> <o> |
Insert a triple into the reserved triples DB |
triple query [-s] [-p] [-o] |
Query triples with optional filters |
log start <name> [--parent <id>] [-f k=v] |
Start a new span; prints its UUID |
log end <id> |
End a span (record ended_at) |
log event <span> <message> [-f k=v] |
Append a log event to a span |
log field <span> <k=v>... |
Set key=value fields on a span |
log tree [--span <id>] |
Print indented span tree |
log list |
List all span UUIDs with name and status |
serve [name] [--file] [-t type] [--ttl] [--wrap] |
Pipe stdin (or --file) into daemon HTTP www; print URL |
web list |
List published temp files |
web url <name> |
Print URL for a published name |
web rm <name> |
Remove a published temp file |
web status |
Print daemon HTTP base URL |
Global flags:
--home <path>/$UNIFIER_HOME— top-level store directory--chroot <name>/$UNIFIER_CHROOT— scope data commands tochroots/<name>/--no-daemon— force direct filesystem access even when a hot daemon is running--namespace <name>/$UNIFIER_NAMESPACE— prefix keys for this invocation (overrides a bound namespace; empty disables it)$UNIFIER_HTTP_PORT— preferred localhost port for the daemon web server (default17355)$UNIFIER_EVENT_TTL_SECS— default event lifetime in seconds (default86400/ 24h;0= never expire)
Examples
# Build
# Shared key-value
# Mailbox messaging
# Agent envelopes + wakeup (Jan listens on events.sock)
# Cron drops
# Chroots
# Process-scoped key prefix (until the script exits or namespace set is called again)
# Managed SQLite
# Triples (auto-creates sqlite/triples.sqlite)
# Span-tree logging
SPAN=
STEP=
# Daemon web server (temp HTML reports)
|
# => http://127.0.0.1:17355/nightly
Installation
The binary is unifier. crates.io uses unifier-cli because unifier is already taken.
Releasing
Same flow as jan-cli. From unifier/:
Monorepo aliases:
This bumps Cargo.toml + CHANGELOG.md, commits those version files, runs tests, then cargo publish as unifier-cli.
Development
Summary of changes
Initial implementation
Created the unifier Rust CLI project with:
- Filesystem-as-store model — inspired by
filesystem-git-issues: entities map to directories, scalar values to files, messages to UUID.txtdrops. - State root at
~/.local/unifierwith$UNIFIER_HOME/--homeoverride. - Key-value store —
put,get,delunderkeys/, with atomic writes forput. - Mailbox —
send,poll,ackfor point-to-point messages undermailbox/<recipient>/. - Cron postbox —
cron,poll-cron,listusing five-field schedule directory names matched against the current clock. - Path safety — keys and recipients reject
..; message ack resolves paths relative to the store root. - Tests — unit tests for cron parsing/matching; integration tests for put/get, mailbox, cron, and root.
project.meta.yaml— monorepo metadata, build scripts, and roadmap for future applications (zshrc hooks, agent pipelines, job queues, etc.).
Chroot support
Added isolated subtrees so multiple programs can share one global store without seeing each other's data:
- On-disk layout —
chroots/<name>/contains a full copy of the layout (keys/,mailbox/,cron/). --chroot/$UNIFIER_CHROOT— all data commands operate only inside the named chroot;UnifierHome::path()returns the effective subtree root.- Admin commands —
chroot init <name>creates a new sandbox;chroot listenumerates existing chroots (always uses the global root). - Scope module (
scope.rs) — centralizes path validation:resolve_under_root()rejects..and absolute escapes; used bylistandack. - Isolation tests — verify keys in one chroot are invisible to the global store and other chroots; path escape attempts fail.
- Backward compatibility — omitting
--chrootpreserves the original global-root behavior.
Source layout
src/
main.rs # binary entry point
lib.rs # library + run()
home.rs # UnifierHome (global + effective root)
scope.rs # chroot path confinement
chroot.rs # chroot init/list
paths.rs # path builders for keys, mailbox, cron
postbox.rs # put/get/send/poll/ack operations
cron.rs # schedule parsing and matching
fs_text.rs # read/write helpers
constants.rs # directory name constants
error.rs # Error type
cli/
defs.rs # clap command definitions
mod.rs # dispatch
tests/
cli_integration.rs
Hot daemon
Added an in-memory hot store with on-demand disk flush:
HotStore(store.rs) — loads keys/mailbox/cron from disk; tracks dirty entries;flush()writes only changed data- Daemon process —
unifier daemon start|run|stop|status|flush|watch; control socket at<store>/.daemon/unifier.sock; event output at<store>/.daemon/events.sock - Mailbox envelopes — messages store
{id, from, to, payload}; send/message/event broadcast a wakeup notice (to+id) so Jan can fetchmailbox/<to>/<id>.txt - Auto-routing — data commands use the daemon when running;
--no-daemonforces direct filesystem access - Idle auto-flush — after 5s with no daemon requests, dirty state is written to disk if 1-minute loadavg ≤ 0.2 (
UNIFIER_IDLE_FLUSH_SECS,UNIFIER_IDLE_MAX_LOAD; set flush secs to 0 to disable). Skips an active tick. - Idle auto-stop — after 10 minutes with no control/event activity, no active tick, and no event subscribers, the daemon flushes and exits (
UNIFIER_IDLE_STOP_SECS; set to 0 to disable). Prevents Gradle-style auto-started daemons from living forever when unused. - Missing-home exit — if the store root directory disappears (e.g. a test
TempDiris dropped), the daemon exits immediately so orphans do not pile up under/tmp. unifier daemon gc— kill orphanunifier daemon runprocesses whose--homepath no longer exists (--dry-runto list only).
Event expiry
Posted events are stored as {payload, created_at, expires_at} under events/<uuid>.json so they do not accumulate forever:
- Default TTL is 24 hours (
$UNIFIER_EVENT_TTL_SECSto change the default;0= never) unifier event '...' --ttl <secs>overrides (--ttl 0never expires)- Payload may set
"ttl": 60(seconds) or"expires_at": "<rfc3339>"(null= never) - Expired files are deleted on daemon load, flush, and a 30s sweep
Process namespaces
namespace set writes a binding for the calling process under .daemon/namespaces/<pid>. Later key operations from that process or a descendant prefix put/get/del and tick lock names with <namespace>/. Bindings are dropped when the process exits (reaped) or namespace clear / a new namespace set runs. Keys starting with / skip the prefix. $UNIFIER_OWNER_PID overrides which process owns the binding (the CLI parent, by default).
Span-tree logging
Added unifier log for tracing Jan cron scripts and agent pipelines:
log start <name> [--parent <id>] [-f k=v]— create a new span underlogs/<uuid>.json; prints UUID for capturelog end <id>— recordended_aton a spanlog event <span> <msg> [-f k=v]— append a timestamped event to a span'seventsarraylog field <span> k=v...— set or update key/value fields on a spanlog tree [--span <id>]— render the full span tree (or subtree) as indented text with events and fieldslog list— list all spans with UUID, name, and open/ended status- Spans load directly from disk (no daemon round-trip); sorted by
started_atfor stable tree output
Daemon web server
Temp static files for Jan report export and quick HTML previews:
serve [name]— read stdin (or--file), write under.daemon/www/, printhttp://127.0.0.1:<port>/<name>--wrap— embed body in Unifier HTML chrome (brand header + “all reports” link)--ttl <secs>— auto-expire published files; GC on HTTP access / periodic sweepweb list|url|rm|status— manage and inspect published files- Daemon binds localhost (preferred port 17355, or
$UNIFIER_HTTP_PORT, else ephemeral); port stored in.daemon/http.port - HTTP activity refreshes the daemon idle timer so open reports are not cut off mid-view
Managed SQLite
Databases live under sqlite/<name>.sqlite (scoped by --chroot when set). sql list / sql tables / sql exec manage and query them directly on disk (no hot-daemon round-trip). The reserved triples database stores unique subject/predicate/object rows; use triple add / triple query or sql exec triples '...'.
Source layout (updated)
src/
store.rs # HotStore (in-memory + flush)
daemon/ # protocol, client, server, lifecycle
...
Roadmap
See project.meta.yaml for the full list. Next major themes:
- Tick-based agents (sense/decide/act lockstep, concurrency modes, differential sensing)
- Factorio factory model: isolated arms coordinated toward a shared system goal