@kedem/okdb 1.8.17 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/bin/okdb.js +1 -1
  2. package/docs/change-log.md +16 -16
  3. package/docs/embeddings.md +1 -1
  4. package/docs/functions.md +26 -25
  5. package/docs/http-cluster.md +157 -0
  6. package/docs/index.md +1 -0
  7. package/docs/manifest.json +5 -1
  8. package/docs/process-registry.md +185 -0
  9. package/docs/processors.md +61 -65
  10. package/docs/queue.md +23 -13
  11. package/docs/subscriptions.md +230 -0
  12. package/docs/worker-fleet.md +139 -0
  13. package/okdb-functions-runner-child.js +1 -1
  14. package/okdb-functions-sandbox-worker.js +1 -0
  15. package/okdb-http-worker-child.js +1 -0
  16. package/okdb-queue-load-handler.js +1 -0
  17. package/okdb-queue-spawn-child.js +1 -1
  18. package/okdb-views-bootstrap-worker.js +1 -1
  19. package/okdb-worker-child.js +1 -0
  20. package/okdb.js +1 -1
  21. package/package.json +1 -1
  22. package/public/sections/db/parts/db-overview.ok.js +1 -1
  23. package/public/sections/embeddings/parts/pipeline-create-panel.ok.js +1 -1
  24. package/public/sections/queue/parts/queue-jobs.ok.js +1 -1
  25. package/public/sections/system/index.ok.html +1 -1
  26. package/public/sections/system/modals/data-ops-modal.ok.js +1 -1
  27. package/public/sections/system/parts/system-process-panel.ok.js +1 -0
  28. package/public/sections/system/parts/system-processing-panel.ok.js +1 -0
  29. package/public/sections/system/parts/system-runtime-overview.ok.js +1 -0
  30. package/public/sections/system/parts/system-workers-panel.ok.js +1 -0
  31. package/types/environment.d.ts +21 -4
  32. package/types/features/embeddings.d.ts +11 -2
  33. package/types/features/queue.d.ts +27 -5
  34. package/types/features/views.d.ts +10 -10
  35. package/types/index.d.ts +54 -6
  36. package/types/options.d.ts +12 -0
  37. package/okdb-fts-handler.js +0 -1
  38. package/okdb-processor-worker.js +0 -1
  39. package/public/sections/processors/index.ok.html +0 -1
@@ -115,22 +115,22 @@ stop();
115
115
 
116
116
  ### Register options reference
117
117
 
118
- | Option | Type | Default | Description |
119
- | -------------------- | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
120
- | `handler` | `function` | required | Async callback receiving `(changes[], info)` |
121
- | `mode` | `string` | `'async'` | `'async'` \| `'inline'` \| `'worker'` |
122
- | `bootstrap` | `string` | `'log'` | `'snapshot'` \| `'log'` — how the initial backfill runs |
123
- | `originMode` | `string` | `'self'` | `'all'` \| `'self'` \| `'remote'` |
124
- | `batchSize` | `number` | `256` | Max changes delivered per handler call |
125
- | `hydrateValues` | `boolean` | `true` | Attach current document value to each change |
126
- | `cursorKey` | `string` | `null` | Persist the cursor under this key so progress survives restarts |
127
- | `lockMode` | `string` | `null` | `'exclusive'` — serialise concurrent flushes for this processor |
128
- | `failOnHandlerError` | `boolean` | `false` | Put the processor into error state on handler exception |
129
- | `flushDebounce` | `number` | `0` | Trailing-edge debounce in ms. Rapid write bursts coalesce into a single flush after `flushDebounce` ms of quiet. `0` = flush immediately (default). |
130
- | `flushInterval` | `number` | `null` | Start a `setInterval` that ticks `_scheduleFlush` every `flushInterval` ms after bootstrap. Useful for polling-style processors or `originMode: 'remote'` subscriptions where local writes don't trigger `_onAfterCommit`. `null` = disabled. |
131
- | `name` | `string` | `null` | Human-readable label shown in admin UI and status |
132
- | `meta` | `object` | `{}` | Arbitrary metadata attached to status objects |
133
- | `leaseTtlMs` | `number` | `30000` | Lease TTL for cross-process exclusive execution |
118
+ | Option | Type | Default | Description |
119
+ | -------------------- | ---------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
120
+ | `handler` | `function` | required | Async callback receiving `(changes[], info)` |
121
+ | `mode` | `string` | `'fanout'` | `'fanout'` (N-of-N) \| `'single'` (1-of-N) \| `'inline'` (every writer). Old names `'async'`→`'fanout'` and `'worker'`→`'single'` are accepted as deprecated aliases. |
122
+ | `bootstrap` | `string` | `'log'` | `'snapshot'` \| `'log'` — how the initial backfill runs |
123
+ | `originMode` | `string` | `'self'` | `'all'` \| `'self'` \| `'remote'` |
124
+ | `batchSize` | `number` | `256` | Max changes delivered per handler call |
125
+ | `hydrateValues` | `boolean` | `true` | Attach current document value to each change |
126
+ | `cursorKey` | `string` | `null` | Persist the cursor under this key so progress survives restarts |
127
+ | `lockMode` | `string` | `null` | `'exclusive'` — serialise concurrent flushes for this processor |
128
+ | `failOnHandlerError` | `boolean` | `false` | Put the processor into error state on handler exception |
129
+ | `flushDebounce` | `number` | `0` | Trailing-edge debounce in ms. Rapid write bursts coalesce into a single flush after `flushDebounce` ms of quiet. `0` = flush immediately (default). |
130
+ | `flushInterval` | `number` | `null` | Start a `setInterval` that ticks `_scheduleFlush` every `flushInterval` ms after bootstrap. Useful for polling-style processors or `originMode: 'remote'` subscriptions where local writes don't trigger `_onAfterCommit`. `null` = disabled. |
131
+ | `name` | `string` | `null` | Human-readable label shown in admin UI and status |
132
+ | `meta` | `object` | `{}` | Arbitrary metadata attached to status objects |
133
+ | `leaseTtlMs` | `number` | `30000` | Lease TTL for cross-process exclusive execution |
134
134
 
135
135
  `stop.status()` (alias `stop.getStatus()`) returns the live processor state including `flushDebounce` and `flushInterval`. `stop.pause()` clears both timers; `stop.resume()` restarts the interval if configured.
136
136
 
@@ -237,7 +237,7 @@ await okdb.embeddings.createPipeline('articles', {
237
237
 
238
238
  ### Queue mode
239
239
 
240
- For high-throughput or external embedder services, the indexer enqueues jobs and a worker pool consumes them concurrently:
240
+ For high-throughput or external embedder services, the indexer enqueues jobs and the worker population claims and processes them concurrently:
241
241
 
242
242
  ```javascript
243
243
  await okdb.embeddings.createPipeline('articles', {
package/docs/functions.md CHANGED
@@ -110,31 +110,32 @@ Use `ctx.log(...)` instead of `console`.
110
110
 
111
111
  ## Runtime model
112
112
 
113
- Functions execute in **isolated child processes** (runners), not in the main OKDB process.
114
-
115
- The pool is **dynamic**:
116
-
117
- - runners spawn on demand when `functions.run()` is called
118
- - runners stay warm between invocations — no spawn overhead on repeated calls
119
- - idle runners are reaped automatically no wasted resources when quiet
120
- - one function runs at a time per runner
121
-
122
- **Per-env isolation**: each environment has its own pool with its own runner processes. Functions in different envs never share a runner.
123
-
124
- **Pool option**:
125
-
126
- - `maxRunners` — maximum number of concurrent runners (optional, defaults to unlimited)
127
-
128
- When using queue-worker engines with `concurrency: N`, the pool grows automatically to match — no manual sizing needed.
129
-
130
- Other behavior:
131
-
132
- - scripts are validated and compile-checked before storage
133
- - timeouts kill and replace the runner
134
- - memory is capped per runner process
135
- - every run is written to a run ledger
136
-
137
- This makes the feature suitable for operational entry points without paying process spawn cost on every call.
113
+ A function invocation is a **durable work unit**, never a closure: `functions.run()` writes a
114
+ `~fn:requests` row and the response comes back as a `~fn:responses` row (the UDP bus carries a
115
+ wake hint so dispatch is ~ms). Because the stored function's script lives in the database, the
116
+ work is fully portable — whoever claims the row can run it. Execution **never blocks the
117
+ caller's main loop**; untrusted user code always runs off-loop.
118
+
119
+ Where it runs depends on whether a [worker population](worker-fleet.md) exists:
120
+
121
+ - **With workers** — a worker claims the request row and executes the user code in its
122
+ **sandbox thread**: a dedicated `worker_thread` with its own okdb instance, heap-capped from
123
+ `runtime.memoryMb` via `resourceLimits`, and `terminate()` as the timeout/wedge watchdog.
124
+ One sandbox per claimer; invocations run concurrently in it, capped. Containment is
125
+ thread-level — a wedged function is `terminate()`d without taking the worker's other claims
126
+ down.
127
+ - **No workers (embedded / scale 0)** — the instance claims its own requests and runs them in a
128
+ **fork pool** of child-process runners (warm between calls, idle-reaped, one run at a time per
129
+ runner). This is the zero-config default and the **memory fence**: a thread heap cap is
130
+ process-fatal on OOM, so only a separate process can contain a memory-hog function
131
+ (`FUNCTION_OOM`) without crashing the host. Embedders who prefer thread-cheapness can opt into
132
+ the sandbox locally; the fork pool can be forced with `OKDB_FN_LEGACY_POOL=1`.
133
+
134
+ The **transport is identical** either way (durable `~fn` rows + bus fast path) — only the
135
+ execution site differs (sandbox thread vs. runner process). Both: scripts are validated and
136
+ compile-checked before storage; timeouts kill/replace the executor; memory is capped; every run
137
+ is written to a run ledger. This makes functions suitable for operational entry points without
138
+ paying spawn cost on every call.
138
139
 
139
140
  ---
140
141
 
@@ -0,0 +1,157 @@
1
+ # HTTP Clustering
2
+
3
+ By default, the `okdb` CLI runs HTTP serving across **multiple worker processes** so that
4
+ request/response latency is never starved by background work (indexing, view rebuilds,
5
+ compaction, async catch-up). The processing tier runs in a single **primary** process; HTTP +
6
+ SSE are served by **N passive worker processes** that share the listen socket.
7
+
8
+ This is a **CLI-only default**. The embedded library (`new OKDB(...).http.listen(port)`) stays
9
+ single-process unless you opt in — see [Embedded clustering](#embedded-clustering).
10
+
11
+ ---
12
+
13
+ ## Process model
14
+
15
+ ```
16
+ ┌──────────────────────────────────────────────┐
17
+ │ primary (full role — engines, async │
18
+ │ processors, compaction:'active', bus)│
19
+ │ • runs ALL processing / indexing / compaction │
20
+ │ • HTTP-SILENT (never serves requests itself) │
21
+ │ • opens its OKDB BEFORE forking (seeds the │
22
+ │ shared __tokenSecret so workers agree) │
23
+ └───────────────┬──────────────────────────────┘
24
+ │ forks N
25
+ ┌───────────────┬───────┴───────┬───────────────┐
26
+ ▼ ▼ ▼ ▼
27
+ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
28
+ │ worker │ │ worker │ ... │ worker │ │ worker │ passive role:
29
+ │ HTTP+SSE│ │ HTTP+SSE│ │ HTTP+SSE│ │ HTTP+SSE│ engines:false
30
+ └─────────┘ └─────────┘ └─────────┘ └─────────┘ asyncProcessors:false
31
+ └───────────────┴── shared listen socket ──┴───────────┘ compaction:'passive'
32
+ bus:true
33
+ ```
34
+
35
+ - **Primary = processor.** Exactly one process runs the processing pool, async processors, and
36
+ compaction. It is HTTP-silent; it never serves requests. (The role flag is the belt; the
37
+ `OKDBLock` proc-lease is the backstop — no worker ever processes.)
38
+ - **Workers = passive HTTP servers.** Each forked worker opens a **passive** OKDB
39
+ (`engines:false`, `asyncProcessors:false`, `compaction:'passive'`, `bus:true`) and serves
40
+ HTTP + SSE on the shared socket. Workers do real reads/writes — inline index/schema/FK/TTL
41
+ updates run synchronously inside the writer regardless of role — but never run background
42
+ processing. The primary's async processors pick up the changelog via the UDP bus poke.
43
+ - **`workers:1`** (or `--no-cluster`) is byte-identical to the pre-cluster single-process path.
44
+
45
+ Worker writes update indexes correctly; the primary's processors then index FTS / refresh
46
+ views / run the materializer / etc. from the shared changelog.
47
+
48
+ ---
49
+
50
+ ## The `workers` knob
51
+
52
+ The CLI resolves the worker count from the first source present, highest priority first:
53
+
54
+ | Priority | Source | Notes |
55
+ | -------- | ------------------- | ---------------------------------------------- |
56
+ | 1 | `--no-cluster` | Forces `workers:1` (single-process escape). |
57
+ | 2 | `--workers N` | Explicit flag. |
58
+ | 3 | `OKDB_HTTP_WORKERS` | Environment variable. |
59
+ | 4 | `http.workers` | `.kdbconfig` config file. |
60
+ | 5 | _default_ | `max(1, cores − 1)` where `cores = os.cpus()`. |
61
+
62
+ A non-integer or `< 1` explicit value clamps to `1` with a warning. The default reserves one
63
+ core for the processing primary and serves HTTP from the rest; on a single-core box it collapses
64
+ to one process.
65
+
66
+ ```bash
67
+ okdb # default: max(1, cores − 1) HTTP workers
68
+ okdb --workers 4 # 4 workers
69
+ okdb --no-cluster # single process (back-compat)
70
+ OKDB_HTTP_WORKERS=3 okdb # 3 workers
71
+ ```
72
+
73
+ ```json
74
+ // .kdbconfig
75
+ { "http": { "workers": 4 } }
76
+ ```
77
+
78
+ ---
79
+
80
+ ## shmbuf requirement (hard gate)
81
+
82
+ Multi-process clustering is **only safe with the native `shmbuf` binding**. The per-env Hybrid
83
+ Logical Clock and changelog clock counter live in a **cross-process shared-memory segment**
84
+ (`shmbuf`). Without the native addon, `shmbuf` falls back to an in-process `SharedArrayBuffer`
85
+ that cannot coordinate clocks across forked workers — two processes could mint the same HLC /
86
+ clock value and corrupt causal ordering.
87
+
88
+ Therefore, **if `shmbuf` is in fallback mode at startup, the CLI forces `workers:1`** regardless
89
+ of any flag, env var, or config, and logs a warning. This gate is non-negotiable.
90
+
91
+ Check readiness with `okdb doctor`:
92
+
93
+ ```
94
+ ✓ cluster: ready (7 workers) # native present
95
+ ✓ cluster: unavailable (shmbuf native missing) — running single-process # fallback
96
+ ```
97
+
98
+ ---
99
+
100
+ ## SSE / log coherence boundary
101
+
102
+ The cluster exposes two distinct event domains. Knowing which is which explains what an SSE
103
+ client on a given worker will and won't see:
104
+
105
+ - **Data changes are cluster-wide.** Each worker runs a **change-feed**: it subscribes to the
106
+ UDP bus `SYSTEM_POKE` signal and, per poke, reads the shared changelog (`getChanges`) to
107
+ reconstruct data-change events (item put/remove, index, view, type). A write committed on
108
+ worker A surfaces in worker B's SSE within the poke window. The bus carries the **signal**;
109
+ the **data** is read from shared LMDB — the changelog is the single source of truth.
110
+ - **Processing progress is primary-owned, forwarded over IPC.** Ephemeral processing events
111
+ (view-rebuild progress, FTS/index lifecycle, processor lifecycle) originate only in the
112
+ primary (the sole processor). The primary forwards a curated set to all workers over
113
+ **cluster IPC**, plus a workers / processing status snapshot. Workers serve
114
+ `/api/processing/status` and `/api/cluster/status` from that snapshot.
115
+ - **Worker-local logs stay local.** A log line emitted inside worker A is **not** replicated to
116
+ worker B's SSE. Logs are per-process; only data changes (via the changelog) and primary
117
+ progress (via IPC) are coherent across the cluster.
118
+
119
+ `/api/cluster/status` returns the cluster shape (primary pid, worker pids, uptime).
120
+
121
+ ---
122
+
123
+ ## Lifecycle
124
+
125
+ - **Worker death → primary respawns** it with bounded backoff (`min(4000, 250·2^failures)` ms)
126
+ and a crash-loop guard (≥ 5 deaths / 10 s per slot stops respawning that slot).
127
+ - **Primary death → workers exit** (no orphan listeners or locks), via `cluster` disconnect plus
128
+ a POSIX `ppid===1` backstop.
129
+ - **Shutdown** drains in-flight requests, then closes envs, with a 3000 ms hard-exit backstop.
130
+
131
+ ---
132
+
133
+ ## Embedded clustering
134
+
135
+ The **library default is single-process** and unchanged. `new OKDB(...).http.listen(port)` with
136
+ no `workers` option returns a plain single-process `http.Server`. To opt an embedded app into
137
+ clustering, pass `workers` (and a passive-worker OKDB factory) to `http.listen`:
138
+
139
+ ```js
140
+ okdb.http.listen(port, {
141
+ workers: 4,
142
+ primaryOkdb: okdb, // full-role processor, opened before listen() (open-before-fork)
143
+ makeOkdb: async () => {
144
+ const w = new OKDB(dbPath, {
145
+ engines: false,
146
+ asyncProcessors: false,
147
+ compaction: 'passive',
148
+ bus: true,
149
+ });
150
+ await w.open();
151
+ return w;
152
+ },
153
+ });
154
+ ```
155
+
156
+ The same shmbuf requirement applies — only cluster an embedded app on a host with the native
157
+ `shmbuf` binding.
package/docs/index.md CHANGED
@@ -108,6 +108,7 @@ await okdb.close();
108
108
  - [Full-Text Search](./fts.md) — FTS index, tokenizer, query
109
109
  - [Embeddings](./embeddings.md) — vector pipeline, semantic search
110
110
  - [Queue](./queue.md) — durable job queue
111
+ - [Live Subscriptions](./subscriptions.md) — signal-SSE change streams with durable sessions
111
112
  - [Files](./files.md) — blob / file storage
112
113
  - [Custom Functions](./functions.md) — sandboxed stored JS functions, env/global scope, HTTP execution
113
114
  - [HTTP API](./http-api.md) — built-in REST API
@@ -18,6 +18,7 @@
18
18
  { "file": "fts.md", "label": "Full-Text Search" },
19
19
  { "file": "embeddings.md", "label": "Embeddings" },
20
20
  { "file": "views.md", "label": "Materialized Views" },
21
+ { "file": "subscriptions.md", "label": "Live Subscriptions" },
21
22
  { "file": "pipelines.md", "label": "Pipelines" },
22
23
  { "file": "queue.md", "label": "Queue" },
23
24
  { "file": "files.md", "label": "Files" },
@@ -31,8 +32,11 @@
31
32
  { "file": "auth-and-sync.md", "label": "Auth & Sync" },
32
33
  { "file": "auth-permissions.md", "label": "Auth Permissions" },
33
34
  { "file": "http-api.md", "label": "HTTP API" },
35
+ { "file": "http-cluster.md", "label": "HTTP Clustering" },
36
+ { "file": "process-registry.md", "label": "Process Registry" },
34
37
  { "file": "plugins.md", "label": "Plugins" },
35
- { "file": "processors.md", "label": "Processors (custom workers)" },
38
+ { "file": "processors.md", "label": "Processors (modes & registration)" },
39
+ { "file": "worker-fleet.md", "label": "Workers (population & placement)" },
36
40
  { "file": "logging.md", "label": "Logging" },
37
41
  { "file": "migration.md", "label": "Migration" }
38
42
  ]
@@ -0,0 +1,185 @@
1
+ # Process Registry
2
+
3
+ The process registry is a root-wide **task manager** for every okdb process sharing one data
4
+ directory: the HTTP-cluster primary and its workers, queue/function child processes, a standalone
5
+ CLI server, and opted-in embedded nodes. It gives the admin UI (System → **Processes**) and the HTTP API
6
+ a single, live view of the whole process tree — and a durable, safe way to control it (pause/resume,
7
+ decommission, retry/reset, hard-kill).
8
+
9
+ It generalizes the same **durable-state-as-coordination** model used by live subscriptions: the
10
+ authority for every action is a durable record; the UDP bus only carries a lossy "go re-read it now"
11
+ hint. A forged or dropped bus packet can therefore neither trigger nor lose a control action.
12
+
13
+ > **Opt-in.** The registry is off by default so embedders and tests are unaffected. `bin/okdb` and the
14
+ > HTTP cluster turn it on automatically; a library embedder opts in with `new OKDB(path, { processes: … })`.
15
+
16
+ ---
17
+
18
+ ## The model
19
+
20
+ ### A process is a `node`; processors are a facet
21
+
22
+ The primary entity is the **process**, not the processor. Each long-lived process publishes one
23
+ `node` row:
24
+
25
+ ```js
26
+ {
27
+ nodeKey, // identity: `${pid}:${startedAt}` (wall-clock startedAt — same-host, see Liveness)
28
+ kind, // primary | http-server | queue-worker | fn-runner | cli | standalone | embedded
29
+ pid, ppid, host,
30
+ supervisorKey, // nodeKey of the supervising process (null = a root) → builds the tree
31
+ slot, slotKey, // supervised position; slotKey is the STABLE durable control key (see below)
32
+ role, // { engines, asyncProcessors, compaction, http }
33
+ listenAddr,
34
+ caps, // { controllable, decommissionable, killable, killConfirm? } — see Capabilities
35
+ lifecycle, // { state: 'running' | 'decommissioning', desiredVersionApplied }
36
+ processing, // FACET (owners only): { pools[], processors[], cap, policy }
37
+ heartbeatAt, expiresAt,
38
+ }
39
+ ```
40
+
41
+ A processor (FTS, views, materializer, time-machine, …) is **not** a separate entity — it is summarized
42
+ on the owner's `processing` facet. The same is true of process-backend pool workers: they are real OS
43
+ processes but are summarized on `processing.pools[]`, not given their own rows.
44
+
45
+ ### Two stores, split by lifetime
46
+
47
+ | Store | Where | Lifetime / durability | Holds |
48
+ | ------------------------- | --------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------- |
49
+ | **Status** (`~processes`) | a dedicated env (`sync:false`, `durability:'fast'`) | **disposable** — crash-loss _is_ the liveness mechanism | one self-refreshed `node` row per live process |
50
+ | **Desired** (`~system`) | `~processes:desired/*`, `~processes:cmd/*` sub-DBs | **durable** — the authority for every control action | pause/decommission/kill intent + retry/reset epochs |
51
+
52
+ `~processes` is off the change-feed (the `~` env name) and is read by key/range, never tailed. Status
53
+ writes emit **no** signal — the panel polls. Only desired-state/command writes emit a `PROC` hint.
54
+
55
+ ### Supervisor-published registration
56
+
57
+ Children (http-servers, queue/fn runners) lack a stable cross-restart identity and some run with the
58
+ bus off, so they do **not** self-register. Their **supervisor** — which already tracks each child's
59
+ pid/slot and heartbeats it — writes the child's `node` row (`publishChildRow`) and removes it on exit.
60
+ Roots (primary/cli/standalone) and opted-in embedded nodes self-publish via their heartbeat.
61
+
62
+ ### Liveness — wall-clock, read-time staleness
63
+
64
+ Each writer refreshes its rows every `OKDB_WORKERS_HEARTBEAT_MS` (default 10000); a row's
65
+ `expiresAt = now + 3×heartbeat`. Readers drop any row whose `expiresAt` is in the past
66
+ (`listNodes` filters at read time) — so a crashed publisher simply ages out; nothing has to notice the
67
+ death. Liveness deliberately uses **wall-clock `Date.now()`**, not the HLC: it is a same-host TTL
68
+ deadline, not a causal stamp, and there is no clock skew between processes on one host.
69
+
70
+ ### `PROC` — a reconcile hint, never a command
71
+
72
+ A control write emits `BUS_EVENTS.PROC` = `{ kind: 'reconcile', scope, key }` on the UDP bus. The bus
73
+ framing is a shared-secret prefix, **not** an authenticated MAC, so the payload carries no command and
74
+ no authority — only "re-read the durable `~system` record for `scope`/`key` now." The owner applies the
75
+ durable record on the hint (fast path) **and** on its heartbeat (backstop), so a dropped hint
76
+ self-heals within one beat.
77
+
78
+ ---
79
+
80
+ ## HTTP API
81
+
82
+ All routes require `system:read` (views) or `system:write` (control).
83
+
84
+ > **Naming.** The canonical surface is **`processes`** — `okdb.processes` and
85
+ > `GET /api/processes[/tree]`. `/api/processes*` is the route surface (the former `okdb.fleet` accessor alias was removed). The tables
86
+ > below still use `/api/processes*`; `/api/processes*` is equivalent for the read views.
87
+
88
+ | Method & path | Purpose |
89
+ | ---------------------------------------------------- | -------------------------------------------------------------- |
90
+ | `GET /api/processes` (or `/api/processes`) | flat list of live processes (grouped, each with `instances[]`) |
91
+ | `GET /api/processes/tree` (or `/api/processes/tree`) | nested process → instance → work tree (`supervisorKey`) |
92
+ | `POST /api/processors/:id/pause`/`resume` | durable pause/resume of a processor (desired-state) |
93
+ | `POST /api/processors/:id/retry` | retry an errored processor (durable command epoch) |
94
+ | `POST /api/processors/cursor/reset` | reset a processor cursor to 0 (durable + command epoch) |
95
+ | `POST /api/processes/:nodeKey/decommission` | gracefully stop a supervised process and keep it down |
96
+ | `POST /api/processes/:nodeKey/recommission` | reverse a decommission/kill — the supervisor re-forks the slot |
97
+ | `POST /api/processes/:nodeKey/kill?force=1` | hard-kill (guarded; see Control tiers) |
98
+
99
+ When the registry is off (single-process / embedders), `/api/processes[/tree]` returns a degenerate
100
+ single-root node so the API still answers.
101
+
102
+ ---
103
+
104
+ ## Control tiers
105
+
106
+ All three tiers write a durable record behind HTTP auth, then emit a `PROC` hint. The owner (or the
107
+ target's supervisor) reconciles it.
108
+
109
+ 1. **Soft — pause/resume** (`controllable` kinds). Durable `~processes:desired/{proc,pool}/<key>` with a
110
+ monotonic `version`; the owner applies it when `version > applied`. Rapid toggles converge to the
111
+ last version (no flip-flop); concurrent writes from two nodes are last-writer-wins by `version`.
112
+ 2. **Imperative — retry/reset** (`~processes:cmd/<logicalKey>`, monotonic `epoch`). The owner re-bootstraps
113
+ the processor via `processor.restart`; `reset` also zeroes the persisted `~proc:state` cursor (so it
114
+ takes effect on the owner's next start even while it is offline). `epoch > applied` guards against
115
+ replay.
116
+ 3. **Lifecycle — decommission / hard-kill** (routed through the **supervisor**, not by fighting the
117
+ respawn loop):
118
+ - **Decommission** (`decommissionable` kinds): the supervisor marks the slot dead (no respawn) and
119
+ cleanly `disconnect()`s the child to drain in-flight work. Keyed by the **stable** `slotKey`
120
+ (`cluster:<port>/<slot>`), so it **survives a supervisor restart** — a restarted primary reloads
121
+ the dead slots before its fork loop and does not revive them. **Recommission** clears the record
122
+ and re-forks.
123
+ - **Hard-kill** (`POST …/kill`): the last resort, guarded. Requires explicit `?force=1`. Rejected
124
+ for non-killable kinds. Killing a **primary** additionally requires `?confirm=1` because it tears
125
+ down the whole cluster (its workers self-exit on the primary's death). The supervisor `SIGKILL`s
126
+ its own child; a root reads its own durable kill record and self-terminates.
127
+
128
+ Every decommission / recommission / kill writes a structured **audit** entry to `~log` (feature
129
+ `workers`: actor, target `nodeKey`+`kind`, action, result), surfaced in System → Logs.
130
+
131
+ ### Capabilities
132
+
133
+ `caps` is published per kind and **enforced by the routes**:
134
+
135
+ | kind | controllable | decommissionable | killable | kill confirm |
136
+ | -------------------------------------------- | ------------ | ---------------- | -------- | ------------ |
137
+ | `primary` | if owner | no (root) | yes | cluster-wide |
138
+ | `cli` / `standalone` | if owner | no (root) | yes | confirm |
139
+ | `http-server` / `queue-worker` / `fn-runner` | no | yes | yes | force |
140
+ | `embedded` | no | no | **no** | — |
141
+
142
+ Roots have no supervisor to route a decommission through — stop a root via its own shutdown or a
143
+ confirmed kill. `embedded` is **observe-only**: okdb must never `process.exit` a host application, so an
144
+ embedded node is never killable and never self-terminates.
145
+
146
+ ---
147
+
148
+ ## Scope: same-host processes vs sync peers
149
+
150
+ The registry covers **same-host, same-root** processes — they share the LMDB files, the UDP bus, and
151
+ the `EnvSharedState` shmbuf. **Cross-machine sync-replication peers are a separate axis** (no shared
152
+ LMDB; "kill" there would be a sync-protocol RPC) and are surfaced by the Sync view, not the process registry.
153
+
154
+ ---
155
+
156
+ ## Embedding
157
+
158
+ ```js
159
+ // Observe-only: appear in the registry, expose nothing controllable.
160
+ const db = new OKDB('./data', { processes: { register: true, kind: 'embedded' } });
161
+
162
+ // A library server that wants the full registry (and is the processor owner):
163
+ const db = new OKDB('./data', { processes: { kind: 'standalone', listenAddr: ':8484' } });
164
+ ```
165
+
166
+ `register: true` makes a process publish its own row even when it is not an `asyncProcessors` owner.
167
+ `bin/okdb` sets `kind` automatically (`primary` / `http-server`); embedders pass it explicitly.
168
+
169
+ ---
170
+
171
+ ## Operational notes
172
+
173
+ - **Bus secret & kill safety.** The `PROC` hint is only a nudge, so a weak/default bus secret cannot
174
+ forge a control action — the durable `~system` record (written behind HTTP auth) is the sole
175
+ authority. Still, set `OKDB_BUS_SECRET` in production so unrelated processes don't cross-poke.
176
+ - **Hard-kill kill-switch.** The irreversible hard-kill tier is enabled by default but can be locked
177
+ down — `OKDB_PROCESS_KILL=off` or `new OKDB(path, { processes: { kill: false } })`. **Recommended in
178
+ production.** When off, every node's `caps.killable` is `false`, so the UI hides the Kill button and
179
+ the route rejects it (409). The graceful, reversible decommission/recommission tier is unaffected.
180
+ - **`okdb doctor`** reports a `processes: N (M supervised)` line when a server is already running on the
181
+ data directory (read as a passive reader; skipped otherwise).
182
+ - **Heartbeat tuning.** `OKDB_WORKERS_HEARTBEAT_MS` (default 10000) sets the publish cadence and, ×3, the
183
+ liveness TTL. Lower it for snappier death detection at the cost of more status writes.
184
+
185
+ See also: [HTTP Clustering](http-cluster.md), [Processors](processors.md).