Repository navigation
Commit 7715d04
fix(cli): os serve says once when it cannot write its runtime state file, naming the file and the OS error (#24234)
Fixes #24091
Clause-②: no
When `os serve` cannot write its runtime state file
(`runtime.ENV.PROJECT.json` in `OS_HOME`), it now says so once, on
stderr, naming the file and the operating system's own error. The boot
still goes on: the file stays best-effort.
## What was wrong
`runtimeBoundPortChannels().writeRuntimeState` in
`packages/cli/src/commands/serve.ts` ended in `catch { /* non-fatal —
supervision file is best-effort */ }` (`:1113` at base `b9ac35cbf`).
That catch printed nothing at any level. The doc of
`BoundPortChannels.writeRuntimeState` (`:887`) says the file is "the
file both of them send a consumer to". So under an uncreatable `OS_HOME`
the banner and the IPC message announced a file that was not there, and
nothing said why.
## What changed
- `serve.ts`, the `writeRuntimeState` catch: one
`console.warn(chalk.yellow(...))` line. It names the file it tried to
write, carries the thrown error's own `message` verbatim (for example
`ENOTDIR: not a directory, stat 'HOME'`), says the server runs on
without the file, and names the remedy (point `OS_HOME` at a writable
directory). When the path itself could not be resolved, the line says
that instead of a file name. Nothing else in the leg moves: the same
`try`, the same write, the same `exit` cleanup.
- The doc of `BoundPortChannels.writeRuntimeState` gains one sentence
(best-effort; the real channel says so once), and the doc of
`runtimeBoundPortChannels` no longer says the failure handling is
"unchanged".
- `packages/cli/test/uncreatable-directory-doors.integration.test.ts`: a
seventh concurrent child and three new assertions (below).
- `.changeset/24091-serve-runtime-state-write-warning.md`:
`@objectstack/cli` patch, `Clause-②: no`.
Landing point: the card's expected one, `writeRuntimeState` itself. It
is the producer: the only code that knows the write failed.
## The PM's four mechanism hypotheses, measured
- **H1 (the silent catch): confirmed** at `b9ac35cbf`, `serve.ts:1113`
as quoted above, and the interface doc at `:887`.
- **H2 (re-measure both inputs at base): confirmed.** Built CLI at
`b9ac35cbf`, Node v22.22.0, a scratch project outside the repo with
`export default {};`, `OS_DATABASE_URL=:memory:`, `--port 0`, each run
in its own process group, killed on the banner tail:
| `OS_HOME` | base `b9ac35cbf` | this branch (`3770601b3`) |
|:--|:--|:--|
| `/proc/os-24091-x` | `Press Ctrl+C to stop` at 2.9 s; 0 lines name the
state file or an error code | banner tail at 3.1 s; 1 line: `⚠ Runtime
state file not written:
/proc/os-24091-fix/runtime.env_local.proj-4d4278c111ec.json — ENOENT: no
such file or directory, mkdir '/proc/os-24091-fix'. The server runs on
…` |
| `SCRATCH/afile/sub`, `afile` a regular file | banner tail at 3.2 s; 0
such lines | banner tail at 3.0 s; 1 line: `…
runtime.env_local.proj-4d4278c111ec.json — ENOTDIR: not a directory,
stat 'SCRATCH/afile/sub'. …` |
(The base probe's one regex hit in both runs was the plugin-list row
`com.objectstack.runtime.default-datasource`, not the state file.) The
`/proc` case no longer hangs at base: PR #24088 (`570c7a2d9`) bounded
that walk.
- **H3 (the stream): stderr, through `console.warn`**, the channel
`serve.ts`'s other boot warnings use (`console.warn(chalk.yellow(' ⚠
…'))`, 26 other sites in the file). `run()` forwards stdout to stderr
for the whole process (`redirectStdoutToStderr`, #7915), and
`console.warn` writes stderr directly, so the line never reaches the
stdio MCP transport's stdout. Measured on both doors above: **0 bytes on
stdout**. The line lands among the other `⚠` boot warnings, right above
`⚠ Server is ready` / the banner. The quiet window is already closed
there (`restoreOutput()` runs before the publish), and it only ever
intercepted stdout, `console.log` and `console.debug`.
- **H4 ("says so once"): structurally once.** `writeRuntimeState` is
reached only through `publishBoundPort`, and `run()` calls
`publishBoundPort` at exactly one site, once per boot.
`serve-bound-port-publication.test.ts` already pins that count: the
comment-masked source holds exactly two `publishBoundPort(` occurrences,
the definition and the one call. There is no rebind path that
re-publishes; an `os dev` restart is a new process and a new boot. So no
once-guard was added.
## Pin
`packages/cli/test/uncreatable-directory-doors.integration.test.ts`, the
file PR #24088 added the serve legs to. It already spawns `bin/run.js`
and is already one of `check-cli-test-child-env.mjs`'s thirteen
built-entrypoint files, so **the pin did not join that population** and
its count label is untouched.
- New leg 7, `serve-notdir`, runs on every platform: `os serve --port 0`
under `OS_HOME=WORKDIR/serve-home-file/sub`, where `serve-home-file` is
a regular file. It must reach `Press Ctrl+C to stop`, and exactly one
output line must name a file under that home with the
`runtime.env_local.` prefix, and that line must contain `ENOTDIR`.
- Leg 5 (`/proc`, Linux only): beside the banner it already asserted,
exactly one line names the state file, with `ENOENT`.
- Leg 6 (CONTROL, creatable home): no output line names the state file.
Assertions are the named subject (the file's path) and the OS code,
never the warning's prose. File wall: 6.29 s for 7 of 7 on the final
head (6.30 s before the merge), under the 10 s line.
## Negative control (ablation)
On committed head `3770601b3`, through `scripts/ablation-replace.mjs` in
wrap mode under the verify lock:
- **Mutation:** anchor `console.warn(chalk.yellow(` plus the next line
(x1 → x0) became `String(chalk.yellow(` (x0 → x1), so the line is built
and never printed. Blob `1e7eca72a6ad` → `4c06596388ab`.
- **Into `dist/`:** `pnpm --filter @objectstack/cli build` exit 0; `node
scripts/ablation-dist-preflight.mjs @objectstack/cli
'String(chalk.yellow('` exit 0, marker present in 1 built file (absent
from `dist/` before the mutation). The pin runs `bin/run.js`, which
resolves `dist/`.
- **Result:** 2 failed, 5 passed: leg 5 and leg 7 each `expected [] to
have a length of 1 but got +0`. Both banner assertions and the CONTROL
legs stayed green, so the boot went on either way and only the line is
missing.
- **Restore:** blob after restore == HEAD (`1e7eca72a6ad`), `git diff
HEAD` empty. CLI rebuilt exit 0; preflight `--absent`: marker absent
from all 652 built files, working tree clean. The pin re-ran 7 of 7
green.
## Tests and gates
Exit codes captured before any pipe. Final head `4fd5c3533` (the branch
merged with `origin/main` `d3d064447` before this PR opened):
- `pnpm --workspace-concurrency=2 --filter '@objectstack/cli...' build`:
exit 0.
- `pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2
--project unit` (the whole unit tier): **exit 0, 310 files, 4,602
tests**, on `4fd5c3533`.
- `pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2
--project integration
test/uncreatable-directory-doors.integration.test.ts`: exit 0, 7 of 7,
on `4fd5c3533`. The diff touches this integration file, so it ran here;
the rest of the integration tier is CI's.
- `pnpm --filter @objectstack/cli run typecheck`: exit 0 (`tsc
--noEmit`, then `check:test-typecheck`: OK, debt unchanged). The pin
file is under `test/`, which `tsconfig.test.json` includes.
- `pnpm exec eslint --no-inline-config --max-warnings=0` over the two
changed `.ts` files: exit 0, no output. The repo-wide run is CI's `Lint
& Repo Gates`.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 67 commands on `4fd5c3533`; all 67
ran one by one. 65 exited 0 at once. `check:dual-build-cjs-loads` and
`check:i18n-coverage` first exited 3 (PREREQUISITE NOT MET: packages
with no `dist/` in this worktree); after `turbo run build
--filter='!@objectstack/docs'` (75 of 76 cached) both re-ran to exit 0
(`111/69/723/1` entries/packages/cjsFiles/probes above the floors; `OK
(14 config(s), 621 baselined untranslated string(s), none new)`).
- `--ran` reconciliation: `67 derived famil(ies) accounted for — 67 run,
0 NOT-MEASURED`, exit 0.
- `pnpm check:cli-test-child-env`: exit 0, 17 spawns of the built CLI,
population unchanged. `pnpm check:nul-bytes`: exit 0.
- The derivation warned that `origin/main` had moved one commit past the
merge (`6e43b7bc3`), changing only `scripts/check-route-envelope.mjs`;
that one family ran its pre-move copy here, and CI runs the current one.
## Acceptance notes
- The warning is `warn`, not `error`, by AGENTS.md's degradation-level
question: the missing file is a functional degradation a supervisor
finds out about on its next read, not a durability claim;
`check:durability-log-level` does not cover this catch.
- The IPC leg's `catch { /* IPC channel closed — best-effort */ }` stays
silent: a closed channel is the ordinary `os serve` case, not a failure.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d702b18 commit 7715d04
3 files changed
Lines changed: 90 additions & 17 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 | |
|---|---|---|---|
| |||
885 | 885 | | |
886 | 886 | | |
887 | 887 | | |
| 888 | + | |
| 889 | + | |
888 | 890 | | |
889 | 891 | | |
890 | 892 | | |
| |||
1074 | 1076 | | |
1075 | 1077 | | |
1076 | 1078 | | |
1077 | | - | |
1078 | | - | |
| 1079 | + | |
1079 | 1080 | | |
1080 | 1081 | | |
1081 | 1082 | | |
1082 | | - | |
| 1083 | + | |
| 1084 | + | |
| 1085 | + | |
1083 | 1086 | | |
1084 | 1087 | | |
1085 | 1088 | | |
1086 | 1089 | | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
1087 | 1093 | | |
1088 | 1094 | | |
1089 | 1095 | | |
| |||
1095 | 1101 | | |
1096 | 1102 | | |
1097 | 1103 | | |
| 1104 | + | |
1098 | 1105 | | |
1099 | | - | |
1100 | | - | |
1101 | | - | |
1102 | | - | |
| 1106 | + | |
| 1107 | + | |
| 1108 | + | |
| 1109 | + | |
1103 | 1110 | | |
1104 | 1111 | | |
1105 | 1112 | | |
| |||
1110 | 1117 | | |
1111 | 1118 | | |
1112 | 1119 | | |
1113 | | - | |
| 1120 | + | |
| 1121 | + | |
| 1122 | + | |
| 1123 | + | |
| 1124 | + | |
| 1125 | + | |
| 1126 | + | |
| 1127 | + | |
| 1128 | + | |
| 1129 | + | |
| 1130 | + | |
| 1131 | + | |
| 1132 | + | |
| 1133 | + | |
| 1134 | + | |
| 1135 | + | |
| 1136 | + | |
| 1137 | + | |
| 1138 | + | |
1114 | 1139 | | |
1115 | 1140 | | |
1116 | 1141 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
34 | | - | |
35 | | - | |
| 35 | + | |
| 36 | + | |
36 | 37 | | |
37 | | - | |
| 38 | + | |
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| |||
50 | 51 | | |
51 | 52 | | |
52 | 53 | | |
53 | | - | |
54 | | - | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
55 | 57 | | |
56 | 58 | | |
57 | | - | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
58 | 66 | | |
59 | | - | |
60 | | - | |
| 67 | + | |
| 68 | + | |
61 | 69 | | |
62 | 70 | | |
63 | 71 | | |
| |||
86 | 94 | | |
87 | 95 | | |
88 | 96 | | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
89 | 107 | | |
90 | 108 | | |
91 | 109 | | |
| |||
164 | 182 | | |
165 | 183 | | |
166 | 184 | | |
| 185 | + | |
| 186 | + | |
167 | 187 | | |
168 | 188 | | |
169 | 189 | | |
170 | 190 | | |
| 191 | + | |
171 | 192 | | |
172 | 193 | | |
173 | 194 | | |
| |||
227 | 248 | | |
228 | 249 | | |
229 | 250 | | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
230 | 254 | | |
231 | 255 | | |
232 | 256 | | |
| |||
236 | 260 | | |
237 | 261 | | |
238 | 262 | | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
239 | 276 | | |
240 | 277 | | |
0 commit comments