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. The package_version in the sample record below is example data, not the current release. 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 |
|
integer |
live managed monitor |
Actual frontend port allocated upward from the backend port plus one. |
|
integer |
live managed monitor |
PID of the daemon-owned frontend process. |
|
float |
live managed monitor |
Epoch seconds identifying that frontend process incarnation. |
|
string |
live managed monitor |
Loopback access link whose fragment carries the monitor’s caller capability. A credential, like |
The qdrant_* fields are absent in local-only mode and when pointed at a remote Qdrant. Treat absent and null alike.
Monitor fields are additive diagnostics and are absent when no managed frontend
is running. Every owner-published snapshot includes the actual assignment, so
heartbeat repair preserves it. The user scratch directory also retains
monitor.json with owner and child PID/start-time witnesses for forced-stop
cleanup. It grants no backend ownership authority. Server stop verifies the
child incarnation before terminating an orphan, then removes its identity.
Parse started_at and last_heartbeat as ISO-8601 strings. Note that qdrant_start_time is epoch seconds, not ISO-8601.
An abridged sample, with the token redacted. It omits always-present fields such as phase_detail, parent_pid, executable, prefix, base_prefix and virtual_env, and every value is sample data:
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, send the independently discovered service_token as a bearer credential to the target port’s /health and verify the authenticated identity response.
Both discovery files are private to the publishing account: mode 0600 on POSIX and a protected user-only DACL on Windows, installed before credential bytes are written. Obtain and refresh credentials only from discovery for the addressed port. Unauthenticated /health omits service_token; a wrong presented credential returns 401. Explicit-port clients without matching readable discovery cannot acquire a token over HTTP. The daemon accepts literal loopback addresses and localhost in Host, and requires any browser Origin to match the request scheme and authority. The monitor’s server-side bridge continues to discover credentials automatically.
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. |
By default the machine pointer is ~/.vaultspec-rag/qdrant-server/service.json, next to service.lock, and the status file is ~/.vaultspec-rag/service.json. 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 |
|---|---|---|
|
The service answers. Its health verdict says whether it is serving. |
0 |
|
Holds the singleton, loading models, not yet serving. |
5 |
|
Nothing is running (resolution |
3 |
|
The recorded process is no longer running. |
4 |
|
The recorded process ID now belongs to another program. |
4 |
|
The process is alive but its port gives no usable answer. |
4 |
|
The service stopped reporting that it is alive. |
4 |
|
The service answers but its models never loaded, so it cannot serve. |
4 |
|
Live holder, untrustworthy pointer (resolution |
4 |
A paused or degraded service still answers on its port, so it is running; its
health says what it is doing. Only a service whose health verdict is error is
lifted to not_serving. Every crashed state and discovery_degraded reuse
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 and flag. Exit codes are in the automation guide.
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.