Skip to content

Commit 225ad58

Browse files
committed
docs(cli): document the two ipc messages and the parent-spawn pattern
Adds a `Waiting for the boot from a parent process` section to the `os dev` page: what each message means, the spawn-with-ipc snippet, the asymmetry (`os dev` consumes `objectstack:listening` and relays only the settle message), and what `objectstack:seed-settled` promises — including why it is sent on suppressed boots with a reason rather than withheld. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
1 parent a81eb6c commit 225ad58

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
`os serve` now announces **`objectstack:seed-settled`** on its existing ipc channel when this boot's seeding has come to rest, and `os dev` forwards it to its own parent process when one holds the channel. A script that spawns a dev server can finally wait for the boot to finish without reading the child's output.
6+
7+
`✓ Server is ready` is true about the HTTP server and says nothing about the app. Seeding races a soft budget (`OS_INLINE_SEED_BUDGET_MS`, default 8s) and past it finishes in the background, so the banner can be a minute ahead of the seed's own result — measured downstream at **82 seconds of silence after the banner, then 120 `ERROR` lines**. The same command on the same corpus settles before the banner on a machine where the seed fits its budget, so the defect is invisible on exactly the boxes that would have caught it. Everything that distinguishes the two cases arrives on the child's inherited stdio, and reading that costs the boot its TTY.
8+
9+
- **The producer is not new.** `@objectstack/runtime` already declares every seed source and settles it at the moment its boot-time write is done, publishing the tally under `@objectstack/spec`'s `seed-settlement` contract. This is the hop outward: the CLI subscribes to two hooks the kernel already fires and reads a snapshot it already publishes. No service is registered and no tally is mutated — the contract is read-only by design.
10+
- **Sent once, and never before `objectstack:listening`.** Seeding that settles during `runtime.start()` is latched and released after the bound port is published, so a parent that waits for the listening message and only then listens for the settle cannot miss it.
11+
-**Keyed on `inFlight`, not `pending`.** Multi-tenant replay and `skipSeedData` register a seed source and deliberately never run it, keeping `pending` above zero for the life of the process. A `pending`-keyed message would never be sent on those boots, and its absence would be indistinguishable from a boot still writing — the same ambiguity this closes, one level up. Those boots get the message with `suppressed` reasons attached instead, so a consumer can say *why* no rows landed.
12+
- **Failure settles too.** A seed that failed has still come to rest; withholding there would recreate the hang. `ok` is a verdict on the per-source counts the boot recorded, and the message carries those counts.
13+
- **The over-budget banner no longer omits seeding.** `Seeds:` is fed by outcomes recorded when a load *finishes*, so past the budget the row was ABSENT and the transcript was byte-identical to an app that declares no seeds — which is how the defect hid. It now reads `pending — N sources still writing`, with a line saying seeding continues in the background; suppressed sources are named rather than reported as pending.
14+
15+
⛔ An ipc channel is **not** made a requirement of either command: `process.send` is undefined under an ordinary terminal boot, both sends are no-ops there, and no byte of that transcript changes. Nothing in the existing `objectstack:listening` publication moves.
16+
17+
Note that `os dev` consumes `objectstack:listening` itself (it is how the bound-port readout and the MCP connect hint learn the real port) and relays only `objectstack:seed-settled`. Spawn `os serve` directly to receive both in one place.

content/docs/deployment/cli.mdx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,67 @@ audit) lands there instead of the business DB (ADR-0057). Opt out with
234234
`OS_TELEMETRY_DB=0`, or point it elsewhere (any mode, including `serve`)
235235
with `OS_TELEMETRY_DB=<path>`.
236236

237+
##### Waiting for the boot from a parent process
238+
239+
`✓ Server is ready` is true about the **HTTP server**, and deliberately says
240+
nothing about the app's data. Seeding races a soft budget
241+
(`OS_INLINE_SEED_BUDGET_MS`, default `8000`); when it runs long the kernel
242+
starts anyway and the rest of the seed finishes **in the background** — so the
243+
banner, and anything that waits for it, can be a minute ahead of the seed's own
244+
result. On a machine where the seed fits its budget the same command settles
245+
before the banner. Both are normal, and which one you get depends on the box.
246+
247+
So a script that spawns a dev server and wants to act **after the boot has come
248+
to rest** should not wait on the banner, and should not need to read the child's
249+
output at all. Spawn with an `ipc` channel and wait for a message:
250+
251+
| Message | Sent by | Means |
252+
|---|---|---|
253+
| `objectstack:listening` | `os serve` | The HTTP server is bound. Carries `{ port, url }` — the port actually bound, which in dev may differ from the one requested. |
254+
| `objectstack:seed-settled` | `os serve` | Nothing is still seeding. Carries `{ ok, suppressed, sources }`. Sent once per boot, always **after** `objectstack:listening`. |
255+
256+
```js
257+
import { spawn } from 'node:child_process';
258+
259+
const child = spawn('os', ['dev'], { stdio: ['inherit', 'inherit', 'inherit', 'ipc'] });
260+
261+
child.on('message', (msg) => {
262+
if (msg?.type !== 'objectstack:seed-settled') return;
263+
if (msg.suppressed.length > 0) {
264+
console.log(`boot complete — seeds not run this boot (${msg.suppressed.join(', ')})`);
265+
} else if (!msg.ok) {
266+
console.log('boot complete — but some seed records did not land; see the log above');
267+
} else {
268+
console.log('boot complete — the app is ready to use');
269+
}
270+
});
271+
```
272+
273+
**`os dev` spawns `os serve`, and the two channels are not symmetric.** `os dev`
274+
consumes `objectstack:listening` itself — it is how the `↪ server bound to port`
275+
line and the MCP connect hint learn the real port — and does **not** relay it.
276+
It forwards `objectstack:seed-settled` to its own parent verbatim. Spawn
277+
`os serve` directly if you need both messages in one place.
278+
279+
An `ipc` channel is optional: without one, both sends are no-ops and nothing
280+
about the command changes. There is no polling to do — if you did not open the
281+
channel, the messages simply are not sent.
282+
283+
<Callout type="info" title="What `objectstack:seed-settled` promises, and what it does not">
284+
It is sent when **nothing is still writing** — on success *and* on failure, since
285+
a seed that failed has still come to rest. Read `ok` together with `sources`
286+
rather than alone: `ok` is a verdict on the per-source counts the boot recorded,
287+
and a source that finished by throwing may record no counts at all.
288+
289+
`suppressed` is non-empty when this boot registered a seed source and
290+
deliberately never ran it — `multi-tenant-replay` (rows are written per
291+
organization on `sys_organization` insert) or `skip-seed-data` (a planning boot
292+
that writes nothing). Those sources never settle and no further signal is
293+
coming for them, which is exactly why the message is sent anyway with the reason
294+
attached: a consumer that waited for *every* source to finish would wait
295+
forever.
296+
</Callout>
297+
237298
#### `os serve`
238299
239300
Starts the ObjectStack server with automatic plugin discovery:

0 commit comments

Comments
 (0)