Repository navigation
Commit 4638625
Fixes #22410
Clause-②: no
The fix fixes the order of two blocks the CLI already prints. No
accepted input, flag, key or export changes. `objectstack:listening` is
the CLI's internal parent/child IPC.
## What changed
`os dev` is two processes writing one terminal. The parent prints the
MCP connect block when the `serve` child's `objectstack:listening` IPC
message arrives. The child used to send that message before it printed
its ready banner, so the two printed at once.
- `packages/cli/src/commands/serve.ts`, `publishBoundPort`: the three
bound-port channels now fire state file, then banner, then IPC (was
state file, IPC, banner). The `BoundPortChannels` field order and docs,
the function's docblock and the call-site comment say why. The state
file still comes first, so the earlier race (the file must exist before
anything announces it) stays closed.
- The order is causal, not timed. The banner is written synchronously to
a terminal (POSIX TTY stdio is synchronous, and `keepStderrNonBlocking`
leaves a TTY alone), so every banner byte is in the terminal before the
message is sent, and the parent prints only on receipt. No sleep, no
timer.
- `dev.ts` is untouched. The child-side reorder alone measured clean
(see the tables), so no single-write change to the parent's block was
needed.
- The message shape `{ type, port, url }` is unchanged. Each place the
child prints its banner is a call to this one function:
`publishBoundPort` has one call site, and boot diagnostics print inside
the banner (`printServerReady` calls `printBootDiagnostics`). The other
`printBootDiagnostics` calls (the `OS_MIGRATE_AND_EXIT` path and the
boot-failure catch) never announce at all.
## Measured before and after, under a real pty
Door: `os dev --ui -p PORT` on the Build-with-Claude-Code tutorial
project. The project is the bundled `blank` template, given the
scaffold's identity rewrite for `support-desk`, plus the page's four
files extracted verbatim and their four barrel lines. `os validate`
prints `Data: 2 Objects 6 Fields`, `UI: 1 Apps 1 Views 1 Actions`, the
page's own output. `@objectstack/*` resolve by symlink to this
worktree's built packages. Each boot ran under `script -qfec` on a fresh
`.objectstack/` and `dist/`, so it is a first boot that seeds the dev
admin. The harness waited for the banner and the block, kept reading for
3 s, then typed a real Ctrl+C into the pty. A "foreign line" is any line
between the block header and its `Disable` row that is not one of the
block's rows.
**Before** (dist built from base `d303b3e7a`, the `serve.js` order read
off the built file as IPC before banner):
| boot | foreign lines inside the block | credential inside the block |
block position | parent rows inside the banner |
|---|---|---|---|---|
| 1 | 2 (`➜ Console:`, `➜ MCP:` rows) | no | inside the banner | 5 |
| 2 | 0 | no | above the banner | 0 |
| 3 | 0 | no | after the banner | 0 |
| 4 | 0 | no | inside the banner | 5 |
| 5 | 0 | no | above the banner | 0 |
| 6 | 0 | no | inside the banner | 5 |
| 7 | 1 (a blank banner line) | no | inside the banner | 5 |
6 of 7 boots printed the block above or inside the banner, 2 of 7 split
the block, 0 of 7 put the credential line inside it. A setup boot before
these seven printed 18 foreign lines between `Skill` and `Connect`: `➜
MCP:`, the whole `🔑 Dev admin: admin@objectos.ai / admin123` section,
`Config` / `Mode` / `Driver` / `Tenancy` / `Plugins: 41 loaded` and
`Press Ctrl+C to stop`.
**After** (dist rebuilt with this fix, the built file's order read as
banner before IPC):
| boot | foreign lines inside the block | credential inside the block |
block position | parent rows inside the banner |
|---|---|---|---|---|
| 1–7 | 0 | no | after the banner | 0 |
7 of 7 clean. A second after-run through the source entry
(`bin/run-dev.js` under tsx) was also 7 of 7 clean.
## The four PM hypotheses
- **H1 (the child): held.** At base, `publishBoundPort` drove
`writeRuntimeState`, then `announceListening`, then `printBanner`. Boot
diagnostics are not printed separately on the success path:
`printServerReady` prints them inside the banner, before `Press Ctrl+C
to stop`. The `printBootDiagnostics` call in the card was in the
boot-failure `catch`, which never announces.
- **H2 (the parent): held.** The block goes to the parent's stdout in
six `console.log` calls; the banner goes to the child's stderr
(`console.error`). In the setup boot the parent printed three block
rows, the child printed about twenty lines, then the parent printed the
last two. So each `console.log` is its own write that another writer can
land between. With the message sent last, no child line printed during
the block in 14 of 14 after-boots, so a single atomic write of the block
was not needed. A child line logged in the instant after the banner
could still land inside the block; nothing in these boots did.
- **H3 (other readers): no reader needs the port earlier.** The readers
of `objectstack:listening` are the `dev.ts` handler (the bound-port
line, the MCP block, the restart line), the unit tests in
`serve-bound-port-publication.test.ts` and
`dev-seed-settled-forward.test.ts`, and
`serve-publishes-bound-port.e2e.test.ts`, which waits for both the
banner and the message. `ServeRestartCoordinator` reads no IPC.
`start.ts` mentions it only in a comment, and
`content/docs/deployment/cli.mdx` documents it as "the HTTP server is
bound", which stays true. The message arrives one banner print later. No
split into two messages.
- **H4 (restart): holds after the fix.** In one `os dev --ui` session
(watch on), 3 restarts were triggered by editing
`src/objects/ticket.object.ts`. Each `✓ server restarted — the new build
is live` line printed right after that restart's `Press Ctrl+C to stop`
(3 of 3), and the MCP block printed once, on the first boot only. The
restart leg was not run against base.
## Pins
- **`packages/cli/test/dev-boot-output-order.integration.test.ts` (new,
integration tier).** It runs the built `os dev --no-watch` on a minimal
fixture under `script -qfec`, 5 boots, each on a fresh database file so
the dev admin is seeded. For every boot it asserts three things: the
block's five rows are contiguous; the `Dev admin` line is present and
outside the block; the block starts after the banner's `Press Ctrl+C to
stop`. A control asserts that every boot printed the block, the banner
tail and the credential line. Without util-linux `script` it fails
rather than skips, as `login-json-ndjson.e2e.test.ts` does.
- Against a dist built from base: red, `4 of 5 boots printed the block
inside or above the banner`. In that run 0 of 5 boots split the block
and 0 of 5 put the credential inside, so the red comes from the order
assertion. `Tests 1 failed | 2 passed (3)`, 76 s.
- Against the rebuilt fix: `Tests 3 passed (3)`, 75 s.
- **`packages/cli/test/serve-bound-port-publish-order.test.ts`
(updated).** The deterministic in-process pin of the sequence now
expects `['state-file', 'banner', 'ipc']`, and its header records the
second constraint. Ablation: `scripts/ablation-replace.mjs` mutated
`serve.ts` back to IPC before banner (anchor 1 to 0, blob `ab5982d524af`
to `727152c21e85`). The test went red, 2 failed and 2 passed: `expected
[ 'state-file', 'ipc', 'banner' ] to deeply equal [ 'state-file',
'banner', 'ipc' ]`. The restore was proven: blob equals HEAD
`ab5982d524af` and `git diff HEAD` is empty.
- **Control:** `src/commands/dev-mcp-connect-hint-origin.test.ts` (the
block still prints, with the right endpoint origin) is in the unit
layer, which passed.
## Tests and gates (at `c0f7c865d`)
- `pnpm --filter @objectstack/cli typecheck`: exit 0.
`check:test-typecheck` passed with its ledger unchanged at 3 files and
28 errors. `tsc -p tsconfig.test.json --listFiles` lists both test files
(1 hit each); its 28 errors are the ledgered ones in 3 other files.
- cli unit layer (`vitest run --project unit`): `Test Files 275 passed
(275)`, `Tests 4057 passed (4057)`.
- cli integration layer (`vitest run --project integration`): `Test
Files 101 passed (101)`, `Tests 934 passed | 2 skipped (936)` (this run
includes the new pin). Run under the verify lock, which it held for
44m47s on a shared box.
- `serve-publishes-bound-port.e2e.test.ts` (nightly tier, run with
`OS_TEST_TIERS=nightly`): `Tests 6 passed (6)`. It is the IPC message's
ordinary consumer and waits for both the banner and the message.
- The 67 commands `dispatch-gates.mjs --commands` derives for this diff
all exited 0, each run once from the worktree root. `--ran`
reconciliation: `67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN`.
`check:dual-build-cjs-loads` and `check:i18n-coverage` first answered
PREREQUISITE NOT MET (exit 3) for 9 packages this diff does not touch.
They were re-run green after a build of those packages, all 58 tasks
restored from the turbo cache.
- Lint, by a narrowing rather than a full run. Each of the 3 changed
`.ts` files is in ESLint's population: `--format json` lists all 3 with
0 errors, 0 warnings and no "File ignored" message. The changeset is
outside it ("File ignored because no matching configuration was
supplied"). `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`, no typed rules) and reads only its two baseline
JSON files, which this diff leaves alone. So the diff cannot move a
verdict on any untouched file. The full `pnpm lint` is CI's.
- Dogfood is declared to CI. `git grep -E 'connect a coding agent|Press
Ctrl\+C to stop|objectstack:listening|Server is
ready|printMcpConnectHint|printServerReady|Dev admin' -- packages/qa/`
exits 1 with no hits. The same tree's dogfood tests do contain
`describe(`.
## Acceptance notes
- To boot the tutorial project on today's `main` the measurement made
two changes to the scratch copy. It stamped `engines: { protocol: '^18'
}`, because the template still carries `^17` while the runtime is
protocol 18; the release's `sync-template-versions` pass stamps that
field. It also set `OS_ALLOW_CONSOLE_DRIFT=1`, because the Console SPA
was the published `@objectstack/console@17.7.0` dist, extracted into the
gitignored `packages/console/dist` and built from a different objectui
sha than the pin. Neither touches the order being measured.
- The before-table counts 0 of 7 credential lines inside the block,
where the card counted 1 of 7. The setup boot did put it inside, and so
did the card's run. The red against base is measured on the order: in 6
of 7 boots the block was above or inside the banner.
- A non-TTY stderr (a pipe whose reader has stalled) is written
asynchronously by `keepStderrNonBlocking`. There the message can still
overtake banner bytes that are waiting in the queue. That case is a
program reading the output, not a terminal; this change leaves it alone.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 86bf9ed commit 4638625
4 files changed
Lines changed: 353 additions & 12 deletions
File tree
- .changeset
- packages/cli
- src/commands
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
731 | 731 | | |
732 | 732 | | |
733 | 733 | | |
734 | | - | |
735 | | - | |
736 | | - | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
737 | 740 | | |
| 741 | + | |
| 742 | + | |
738 | 743 | | |
739 | 744 | | |
740 | 745 | | |
| |||
770 | 775 | | |
771 | 776 | | |
772 | 777 | | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
773 | 797 | | |
774 | 798 | | |
775 | 799 | | |
| |||
796 | 820 | | |
797 | 821 | | |
798 | 822 | | |
799 | | - | |
800 | | - | |
801 | | - | |
| 823 | + | |
802 | 824 | | |
803 | 825 | | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
804 | 829 | | |
805 | 830 | | |
806 | 831 | | |
| |||
5331 | 5356 | | |
5332 | 5357 | | |
5333 | 5358 | | |
| 5359 | + | |
5334 | 5360 | | |
5335 | 5361 | | |
5336 | | - | |
5337 | 5362 | | |
5338 | 5363 | | |
5339 | 5364 | | |
5340 | | - | |
5341 | | - | |
| 5365 | + | |
| 5366 | + | |
| 5367 | + | |
| 5368 | + | |
5342 | 5369 | | |
5343 | 5370 | | |
5344 | 5371 | | |
| |||
Lines changed: 293 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
0 commit comments