Skip to content

Commit 7dd12ec

Browse files
committed
feat(cli): announce seed settlement on serve's ipc channel and forward it from dev
`✓ Server is ready` is true about the HTTP server and silent about the app. Past the inline seed budget the rest of the seed runs detached, so the banner — and any parent that waits for it — can be eighty seconds ahead of a hundred-line error wall, and nothing a parent can observe distinguishes that boot from one that seeded cleanly. `serve` now sends a second ipc message, `objectstack:seed-settled`, beside `objectstack:listening`, latched so it can never precede it; `os dev` relays it verbatim to its own parent when one holds the channel. The producer is not new: the runtime already publishes the tally under the spec's `seed-settlement` contract, and this is the hop outward. Keyed on `inFlight === 0`, not `pending === 0`: multi-tenant replay and `skipSeedData` keep `pending` above zero for the life of the boot, so a `pending`-keyed message would never be sent there and its absence would be indistinguishable from a boot still writing. Those boots get the message with `suppressed` reasons instead. The banner's over-budget path now says `Seeds: pending` and that seeding continues in the background, so the transcript is no longer byte-identical to a boot that declared no seeds. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
1 parent 272c04b commit 7dd12ec

3 files changed

Lines changed: 432 additions & 0 deletions

File tree

‎packages/cli/src/commands/dev.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,50 @@ export function printMcpConnectHint(
131131
console.log(chalk.dim(' Disable OS_MCP_SERVER_ENABLED=false'));
132132
}
133133

134+
/**
135+
* The hop outward: relay the `serve` child's `objectstack:seed-settled`
136+
* announcement to `os dev`'s OWN parent (#17329).
137+
*
138+
* ## Why the hop exists at all
139+
*
140+
* `os dev` is a spawner. It runs `serve --dev` over
141+
* `stdio: ['inherit','inherit','inherit','ipc']`, so the child's settle
142+
* announcement lands HERE and stops — while the consumer that needs it (a demo
143+
* script, a test harness, anything that spawns `os dev` and wants to print one
144+
* line after the boot) holds a channel to `os dev`, not to a grandchild process
145+
* it did not start and cannot name. One hop is the whole of the missing piece:
146+
* the producer already exists, and the child already announces.
147+
*
148+
* ## Relayed verbatim, deliberately
149+
*
150+
* ⛔ Nothing here re-derives, re-summarises or re-grades the message. The child
151+
* read the settlement tally off the kernel that did the seeding; this process
152+
* has no kernel and could only guess. Passing the object through means `os
153+
* dev`'s parent and the `serve` child can never be made to say two different
154+
* things about one boot — the same rule the `MCP:` row above follows for the
155+
* origin, and for the same reason.
156+
*
157+
* ## An IPC channel stays OPTIONAL for this command
158+
*
159+
* ⛔ A parent that holds no channel must be unaffected, and is: `process.send`
160+
* is `undefined` under an ordinary terminal `os dev`, so this returns having
161+
* done nothing, printed nothing, and changed no byte of that transcript. The
162+
* `serve` child's own `announceListening` is best-effort for exactly this
163+
* reason and this is its mirror — ⛔ this message does not make an IPC channel
164+
* a requirement of running a published command.
165+
*
166+
* @returns `true` when the message was a settle announcement (handled here, and
167+
* the caller should stop) — `false` for every other message, which the
168+
* caller's own branches still own.
169+
*/
170+
export function forwardSeedSettledToParent(msg: unknown): boolean {
171+
if ((msg as { type?: unknown } | null | undefined)?.type !== 'objectstack:seed-settled') return false;
172+
try {
173+
if (typeof process.send === 'function') process.send(msg);
174+
} catch { /* the parent's channel closed — best-effort, exactly like the child's */ }
175+
return true;
176+
}
177+
134178
export default class Dev extends Command {
135179
static override description =
136180
'Start development mode — watch sources, rebuild the artifact, and restart the server on change';
@@ -566,6 +610,10 @@ export default class Dev extends Command {
566610
// its HTTP server is up. We surface it so the printed URL is correct
567611
// even when the port was auto-shifted (e.g. 3000 busy → 3001).
568612
child.on('message', (msg: any) => {
613+
// #17329 — the hop outward. Handled first and exclusively: a settle
614+
// announcement carries no port and has nothing to do with the block
615+
// below. See {@link forwardSeedSettledToParent}.
616+
if (forwardSeedSettledToParent(msg)) return;
569617
if (msg?.type === 'objectstack:listening' && msg.port) {
570618
const actual = String(msg.port);
571619
if (actual !== requestedPort) {

0 commit comments

Comments
 (0)