vaultspec-rag
Service discoveryLink to Service discovery
The resident background daemon is a machine singleton: exactly one may run per machine, because it owns the single GPU and the single managed Qdrant server. Sibling tools locate that daemon, and judge whether it is alive, by reading the discovery records it publishes. Those tools are the command-line interface (CLI), the Model Context Protocol (MCP) server, and any supervising broker. For what the daemon itself does, see the architecture overview.
This document is the consumer-facing contract for those records. It covers:
the two discovery views and where each lives
the versioned schema
who owns the records and how ownership is proved
the fields you may rely on
the typed states you must distinguish
The last of those matters most: the difference between no daemon and a live daemon whose address cannot be trusted.
Fields not listed under Interface fields are internal diagnostics. Don’t rely on them.
Throughout, the daemon is the serving process, the machine pointer and the status file are the two records, and a consumer is any tool reading them.
Resolving a daemonLink to Resolving a daemon
The contract below decomposes into one ordered procedure. A consumer resolves a daemon by:
Probing the machine lock. A live holder is proof that something owns the singleton.
Reading the machine pointer. Falling back to the status file only when no lock is held.
Validating the
schemaandversionpair, and refusing a record it does not understand.Checking freshness:
now - last_heartbeatagainststale_after_s.Confirming the pointer’s
pidmatches the lock holder.
Each step has its own section. The result is one of three typed states.
Version discriminatorLink to Version discriminator
Every record carries a schema discriminator. Pin on the pair and refuse a record you don’t understand:
Field |
Type |
Value |
|---|---|---|
|
string |
|
|
integer |
|
A breaking shape change bumps version, and the same change updates this document. Additive fields don’t bump it.
A consumer that doesn’t understand the pair must refuse the record. This project’s own client enforces the pin rather than only writing it: it refuses a record declaring a schema or version this build doesn’t recognize, and reads none of the remaining fields on the assumption that they mean what it expects.
A record declaring neither is the pre-discriminator case. Accept it; the next heartbeat upgrades it in place. A record declaring one half without the other is a partial write. Refuse it.
Don’t confuse this pair with package_version. The pair describes the shape of the record; package_version describes the release of the daemon that wrote it. Compare the pair to decide whether you can read the record at all, and package_version to decide whether you may drive the daemon it points at.
Interface fieldsLink to Interface fields
Both views carry the same payload. “Presence” says when a field is absent.
Field |
Type |
Presence |
Meaning |
|---|---|---|---|
|
string |
always |
Schema discriminator. |
|
integer |
always |
Schema version. |
|
integer |
always |
Operating-system process id (PID) of the serving daemon. See the PID-reuse caveat. |
|
integer |
always |
TCP port the daemon serves on. |
|
string |
always |
ISO-8601 timestamp of the first publication. |
|
string |
always |
ISO-8601 timestamp of the most recent heartbeat. |
|
integer |
always |
Seconds between heartbeats. Fixed at 15; see below. |
|
integer |
always |
Seconds after which a consumer treats the record as stale. Fixed at 60; see below. |
|
string |
always |
Bearer token for the daemon’s token-gated HTTP routes. |
|
string |
always |
Release of the daemon that wrote the record. |
|
string |
always |
Python running the daemon. |
|
string |
always |
Lifecycle phase, for example |
|
string |
always |
Human-readable detail beside |
|
integer |
always |
PID of the process that spawned the daemon. |
|
string |
spawned services |
Per-spawn identifier the launcher passes in and matches back, so it can tell the process it started from another that was already coming up. |
|
string |
always |
Absolute path to the interpreter running the daemon. |
|
string |
always |
Environment prefix of that interpreter. |
|
string |
always |
Base installation prefix, which differs from |
|
string |
always |
Active virtual-environment path, or |
|
integer |
managed server only |
PID of the managed Qdrant process. |
|
boolean |
managed server only |
Whether the daemon last observed that process alive. |
|
integer |
managed server only |
Port the managed Qdrant serves on. |
|
string |
managed server only |
Version of the managed Qdrant binary. |
|
float |
managed server only |
Epoch seconds when the managed Qdrant started. |
|
object |
managed server only |
The witnessed child’s |
The qdrant_* fields are absent in local-only mode and when pointed at a remote Qdrant. Treat absent and null alike.
Parse started_at and last_heartbeat as ISO-8601 strings. Note that qdrant_start_time is epoch seconds, not ISO-8601.
A complete record, with the token redacted:
File contents
{
"schema": "vaultspec.rag.service",
"version": 1,
"pid": 46220,
"port": 8766,
"started_at": "2026-09-02T13:14:29+00:00",
"last_heartbeat": "2026-09-02T14:51:04+00:00",
"heartbeat_interval_s": 15,
"stale_after_s": 60,
"service_token": "<redacted>",
"package_version": "0.4.21",
"python_version": "3.13.14",
"phase": "running",
"qdrant_pid": 44568,
"qdrant_alive": true,
"qdrant_port": 8765,
"qdrant_version": "1.19.0",
"qdrant_start_time": 1788354870.6777313,
"qdrant_identity": { "pid": 44568, "port": 8765, "start_time": 1788354870.6777313 }
}
Typed resolution: ready, absent, degradedLink to Typed resolution: ready, absent, degraded
Discovery is more than a port check. Resolution returns one typed, evidence-carrying verdict whose state is exactly one of three values.
ready: a live lock holder has published a schema-valid, fresh pointer whose PID matches the holder.portcarries a usable address.absent: no machine lock is held, so nothing is running. Only in this case does a consumer consult the status-file legacy fallback.degraded: a live holder owns the singleton, but its published pointer cannot be trusted.
degraded is deliberately distinct from absent, because something owns the singleton. On a degraded verdict a consumer must not:
render the daemon as stopped
fall back to a status-file address the owner never published
start a second daemon, which would only lose the race
A degraded verdict carries a reason naming the specific disagreement:
|
Meaning |
|---|---|
|
Live holder, but no pointer record. |
|
Pointer present, but its port is unreadable or malformed. |
|
Pointer’s |
|
Pointer names a PID other than the live holder, a leftover from a previous incarnation. |
|
The machine lock or pointer could not be inspected at all. |
|
Pointer declares a |
The verdict also preserves holder_pid, pointer_pid, port, service_token, heartbeat_age_s, and stale_after_s as evidence. Its source records which view supplied the address: machine_pointer, status_file, or none. A status_file-sourced ready verdict is a labeled legacy compatibility result, valid only because no live holder exists.
Staleness contractLink to Staleness contract
The daemon rewrites last_heartbeat every heartbeat_interval_s seconds. Treat the daemon as stale, and not live, when now - last_heartbeat > stale_after_s.
Read both thresholds from the record rather than hard-coding them. If the payload predates stale_after_s, fall back to a 60-second window.
Today the daemon publishes 15 and 60. Neither is configurable - there is no environment variable, flag, or config key for either - so a consumer that wants a different window enforces it on its own side rather than looking for a setting that does not exist. Reading them from the record is what keeps that true if the daemon’s values ever change.
PID-reuse caveat. After a crash without clean shutdown, a recorded pid may belong to an unrelated process. Don’t treat a live pid alone as proof the daemon is up. Combine it with a fresh last_heartbeat. Where you need stronger proof, verify the service_token against the target port’s /health response.
The two discovery viewsLink to The two discovery views
The daemon publishes the same versioned payload to two files, both named service.json, in two directories. They serve different consumers.
View |
Location |
Authoritative |
Purpose |
|---|---|---|---|
Machine pointer |
Beside the lock file |
Yes, mutated only by the lock owner |
The canonical address record. A consumer that doesn’t share the daemon’s status directory still finds the one running daemon. |
Status file |
|
No, an operator and compatibility view |
Operator detail, and a legacy fallback for daemons predating the pointer. |
The configuration reference covers how status_dir resolves.
Deleting or corrupting the status file never stops the daemon from republishing canonical discovery: each heartbeat rebuilds a complete daemon-owned snapshot and recreates a missing or invalid status file.
Ownership and authenticated publicationLink to Ownership and authenticated publication
The machine pointer is owner-authenticated. A caller may publish or delete it only while it can present the process-local lease returned by a successful lock acquisition. The publish primitive verifies that the current process still holds that exact retained lease, and that the payload names the lease-owning PID. A PID read or a lock probe alone is not sufficient authorization. So a non-holder cannot overwrite or remove canonical discovery even if it reproduces the owner PID.
Publication is atomic. The writer creates a unique temporary file in the destination directory and replaces it into place, so a consumer never observes a partial document.
Parse both files structurally, not by layout. Their on-disk formatting differs: the machine pointer is pretty-printed, while the status-file writer emits compact JSON.
Startup writers to the status file serialize through an operating-system-backed lock, so either the launching CLI or the daemon may publish first, and the later writer merges rather than erasing fields the other already published. The daemon’s same-port lifecycle publication is authoritative. On Windows a virtual-environment launcher’s PID may differ from the serving daemon’s, so once the daemon has stamped phase, a delayed parent write preserves the daemon PID, the first started_at, and the managed-Qdrant identity.
Operator statusLink to Operator status
Every adapter renders one canonical operator verdict, composed from the typed resolution plus already-probed liveness signals. The verdict is derived once and rendered per surface, never recomputed.
Operator state |
Meaning |
Exit code |
|---|---|---|
|
Serving normally. |
0 |
|
Holds the singleton, loading models, not yet serving. |
5 |
|
Nothing is running (resolution |
3 |
|
A recorded daemon is not serving: dead PID, reused PID, silent port, or stale heartbeat. |
4 |
|
Live holder, untrustworthy pointer (resolution |
4 |
degraded_discovery reuses exit code 4, so a supervising broker needs no new code. The structured status body carries a discovery block with the resolution’s state, source, both PIDs, port, heartbeat age, staleness window, reason, and a one-line evidence string.
Operations that depend on the daemon fail fast on a degraded resolution rather than guessing an address. Read-only status returns the complete observation.
For the operator-facing commands that render these states, see the service mode guide and the CLI reference.
ReconcileLink to Reconcile
A consumer cannot repair a degraded machine: the singleton owner is the only writer of canonical discovery. vaultspec-rag server reconcile waits for the owner to republish. It is non-destructive and idempotent, and it never stops, restarts, or terminates a process.
It re-resolves on an interval until its timeout. It succeeds only when the serving daemon’s identity agrees across every axis the pointer claims: holder PID, pointer PID, freshness, port, service token, and the live /health response. It exits 0 once discovery agrees and 1 if it does not converge in time.
A wedged owner stays degraded, and the operator sees that state rather than a guessed recovery. For the command’s flags, see the CLI reference.
Where to go nextLink to Where to go next
Architecture answers what the daemon does and why it is a singleton.
Service mode answers how to start, observe, and stop the daemon.
Configuration answers how the status directory and ports resolve.
MCP integration answers how the MCP server reaches the daemon.
CLI reference catalogues every command, flag, and exit code.
Glossary defines the vocabulary used here.
If you find a wedged owner that never recovers, or a record this contract does not describe, the issue tracker takes questions as well as bug reports.