Repository navigation
fix(cli): os serve no longer hangs on a runtime state file directory that can never be made - #24088
Conversation
…oping in recursive mkdir Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT
… not-covered line it closes Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 29 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3d6d5a563035bacebd80e7d293779921c2d27181 && git checkout 3d6d5a563035bacebd80e7d293779921c2d27181
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin be076dad28a0d35cefc7f76cd06950620cb8b81b c31899a8a70fc10c6564489b680dbd22a6ba1a0e && git checkout -B drift-repro be076dad28a0d35cefc7f76cd06950620cb8b81b && git merge --no-ff c31899a8a70fc10c6564489b680dbd22a6ba1a0e
node scripts/docs-audit/affected-docs.mjs --json be076dad28a0d35cefc7f76cd06950620cb8b81b |
…vers the state file that note left uncovered Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT
席位核对ACCEPT · #23858 (round 2, the remainder after PR #23979) ·
Filed from this round:
Carried to the release owner: the round-1 note's "Not covered yet" line, at Landing: Generated by Claude Code |
Fixes #23858
Clause-②: no
The card's remainder after PR #23979 (which landed as
Part of):os serve's runtime state file directory, plus the two in-package copies of the bounded walk. The state-file site now goes through the CLI's module-internalmakeDirectorySync. The two copies are kept, not folded, by the claim's own rule: a fold ontomakeDirectorySynccannot keep their answers and error text byte for byte (measured below).What was wrong
runtimeBoundPortChannels().writeRuntimeStateinpackages/cli/src/commands/serve.tsmadeOS_HOMEwithfs.mkdirSync(path.dirname(runtimeFile), { recursive: true }). Node's recursivemkdir(v22.22.0) never returns when a level answersENOENTwhile its parent exists, as/procanswers for a name it does not have.publishBoundPortwrites the state file after the bind and before the banner, so the server held its port, printed noPress Ctrl+C to stop, sent no listening message to anos devparent, and answered nothing.What changed
serve.ts: that one call is nowmakeDirectorySync(path.dirname(runtimeFile)), imported from../utils/make-directory.js. Nothing else in the file moves. The refusal lands in the write's existing best-effortcatch, so the boot goes on without the file, as it already did for every other refusal (see Acceptance notes: nothing tells the operator).packages/cli/test/uncreatable-directory-doors.integration.test.tsgains two legs (below). No new spawning file, so thecheck:cli-test-child-envpopulation is unchanged..changeset/23858-serve-state-file-directory.md:@objectstack/clipatch,Clause-②: no..changeset/23858-uncreatable-directory-refusal.md(PR fix(cli,runtime): refuse an uncreatable state, output or cache directory at once instead of looping in recursive mkdir #23979's pending note) is NOT in this diff: it was restored byte-for-byte from the merge base by seat ruling B (6227399342). This PR's changeset says that it covers the runtime state file that note lists as "Not covered yet".The two folds: kept, measured
The claim says each fold keeps its callers, its answers and its error text byte for byte, or the copy is kept. Measured function by function on this branch: each copy as it stands, against the fold as it would be written (each copy's own wrapper and message, verbatim, around its package's
makeDirectorySync).dev=prepareDevDatabaseDirectory(dev.ts);runtime=makeSqliteDatabaseDirectory(standalone-stack.ts), reached throughcreateStandaloneStackbecause it is module-private./proc/MISSING/aENOENT, same textENOENT, same textENOTDIR, same textENOTDIR, same textEEXIST, same textEEXIST, same textEEXISTEEXISTENOENT … mkdir 'ROOT/dl2/sub'; foldEEXIST … mkdir 'ROOT/dl2'Two causes, both in the walk itself, so neither wrapper can absorb them:
EEXISTon a created level. The copies accept anyEEXIST;makeDirectorySyncaccepts it only when a directory stands there (PR fix(cli,runtime): refuse an uncreatable state, output or cache directory at once instead of looping in recursive mkdir #23979's acceptance notes named this). A dangling symlink answersEEXISTtomkdirandENOENTtostat.awaiteachstatandmkdir. Measured onprepareDevDatabaseDirectory, event-loop turns that ran while the walk was pending: 46 for a three-level create, 100 for/proc, against 0 for the fold.So both copies stay, byte for byte. Neither file is in this diff.
What the copies' answer costs at the door (base
b6805ac0f, built CLI; unchanged here):OS_HOME=SCRATCH/dl-home os dev, whereSCRATCH/dl-home/datais a dangling symlink, exits 1 after 11.2 s with✗ Plugin com.objectstack.auth failed to start - rollback complete: The database refused to run this query for object 'sys_organization'. …. Nothing names the file, the directory or the symlink.OS_DATABASE_URL=file:SCRATCH/dl-db/link/ctl.db os serve, wherelinkis a dangling symlink, exits 1 after 5.3 s with✗ Plugin package-service failed to start - rollback complete: The database refused to run a raw statement. ….EEXIST. That is a change of answer, which the claim's rule keeps out of this PR. It is in the report for the seat.Measured at the public door
Built CLI, Node v22.22.0, a scratch project outside the repo with
export default {};(and adefineStackproject foros dev). Each run is in its own process group, killed whole at the bound.strace -f -c -e trace=mkdir,mkdiratcounts the calls.b6805ac0fOS_HOME=/proc/os-23858-r2 OS_DATABASE_URL=:memory: os servemkdirin 20 s (allmkdir("/proc/os-23858-r2")/mkdir("/proc")), no banner, killed at the boundPress Ctrl+C to stopat 4.7 s; 1 failingmkdirin the whole runOS_HOME=/proc/os-23858-r2 os dev --database :memory: --no-watch(the serve child reaches the same write)mkdirin 40 s, no bannermkdirOS_HOME=/proc/os-23858-r2 os serve(SQLite default insideOS_HOME)✗ [StandaloneStack] SQLite database file '/proc/os-23858-r2/data/objectstack.db' cannot be created: … ENOENT …OS_HOME=/proc/os-23858-r2 os start(also withOS_DATABASE_URL=:memory:)✗ Cannot create home directory at /proc/os-23858-r2: ENOENT …(PR #23979's door, before any serve child)OS_HOME=/proc/os-23858-r2 os dev(unified default)✗ The default database '/proc/os-23858-r2/data/objectstack.db' cannot be created: its directory '/proc/os-23858-r2/data' could not be made: ENOENT: no such file or directory, mkdir '/proc/os-23858-r2'OS_DATABASE_URL=file:/proc/os-23858-r2-db/a/ctl.db os serve✗ [StandaloneStack] SQLite database file '/proc/os-23858-r2-db/a/ctl.db' cannot be created: its directory '/proc/os-23858-r2-db/a' could not be made: ENOENT: …, mkdir '/proc/os-23858-r2-db'os startnever reaches the state-file write under an uncreatable home: it makesHOME_DIR/datafirst and refuses there. The door that reaches it isos serve(oros dev's serve child) with the database outsideOS_HOME.The remaining recursive
mkdirsites in the two packagesgit grep -n "recursive: true"overpackages/cli/srcandpackages/runtime/srcat baseb6805ac0f, test files excluded: 18 lines. 3 arermSync, 2 arereaddirSyncin doc comments, and 6 are doc comments naming the old call. That leaves 7mkdircode sites; a multi-line search (mkdir(Sync)?\(withoutrecursiveon the line) finds only the walks' own non-recursive calls.cli/src/commands/serve.ts:1097OS_HOME(the state file)cli/src/commands/create.ts:740targetDir, just made bymakeDirectorySynccli/src/commands/dev.ts:572,:577--freshhome'sdataanduploads, one level undermkdtempSynccli/src/commands/init.ts:1122targetDir, just written intocli/src/utils/build-runtime.ts:198outputDir; its only caller passescompile'sartifactDir, made or refused bymakeDirectorySynccli/src/utils/dev-organization-ownership-upgrade.ts:128packages/runtime/srchas no recursivemkdirleft.Pins
packages/cli/test/uncreatable-directory-doors.integration.test.ts, two new legs among its concurrent children (6 in all), each its own process group, killed whole at 60 s:os serve --port 0underOS_HOME=/proc/MISSINGwithOS_DATABASE_URL=:memory:printsPress Ctrl+C to stopinside the bound. The group is killed on that text.OS_HOMEthree levels deep prints the banner, and by then the home holds exactly oneruntime.env_local.PROJECT.jsonwhosepidis the child's own.File wall 6.38 s (6 of 6), under the 10 s line.
--port 0keeps both off any fixed port.Negative control (ablation)
On committed head
b3fa3f11d, throughscripts/ablation-replace.mjsin wrap mode under the verify lock:makeDirectorySync(path.dirname(runtimeFile));(x1 → x0) becamefs.mkdirSync(path.dirname(runtimeFile), { recursive: true });(x0 → x1), blob87014c3470c7→f238ec2b0a69.dist/:pnpm --filter @objectstack/cli buildexit 0;node scripts/ablation-dist-preflight.mjs @objectstack/cli 'dirname(runtimeFile), { recursive: true }'exit 0, marker present in 1 built file. The door pin runsdist/.'serve-proc' had not settled after 60000 ms and was killed — a directory prep is looping again.Both serve and start CONTROLs stayed green.87014c3470c7),git diff HEADempty. CLI rebuilt exit 0; preflight--absent: marker absent from all 652 built files, working tree clean. The pin re-ran 6 of 6 green (6.52 s).Tests and gates
Exit codes captured before any pipe. After merging
origin/main(8133cc780) into the branch, headfd250408f:pnpm exec turbo run build --filter='@objectstack/cli...' --concurrency=2: exit 0 (61 tasks, 27 cached).pnpm --filter @objectstack/cli run typecheck: exit 0.pnpm --filter @objectstack/runtime run typecheck: exit 0. The pin file is intsconfig.test.json's program (--listFiles) with 0 errors.pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 --project unit(the whole unit tier): exit 0, 308 files, 4,582 tests. It includessrc/commands/dev-database-directory.test.ts,src/utils/make-directory.test.ts,serve-bound-port-publication,serve-no-transport-announcements,serve-bound-port-publish-orderandserve-publishes-bound-port.e2e.--project integrationfortest/uncreatable-directory-doors.integration.test.tsandtest/serve-runtime-state-project-key.test.ts(the other state-file pin): exit 0, 2 files, 18 tests.pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 --project local src/standalone-stack-uncreatable-db-directory.test.ts src/make-directory.test.ts(the runtime copy's own pin, unchanged, and the runtime walk's): exit 0, 2 files, 12 tests.pnpm exec eslint --no-inline-config --max-warnings=0over the two changed.tsfiles: exit 0. The repo-wide run is CI'sLint & Repo Gates.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 67 commands, all run one by one onfd250408f: 66 exit 0, andnode scripts/check-empty-changeset.mjs --base origin/mainexit 1 there (the pending-note edit, since restored). Onc31899a8aall 67 are accounted for with exit 0, and CI'sCheck Changesetis success.check:type-check-debt, whose closure build had not got the verify lock, re-run under the lock: exit 0,--re-measure: OK — 1 ledger entr(ies) re-measured … none above its recorded number. That build also built the packagescheck:dual-build-cjs-loads(exit 0, 110 require entry points across 68 packages load) andcheck:i18n-coverage(exit 0, 14 configs, none new) had found missing.--ranreconciliation:67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED, exit 0.node scripts/check-nul-bytes.mjs: exit 0.packages/cli's integration tier (no other integration file and no spawn entry is touched), and the runtime package's suites (the runtime is not in this diff).Acceptance notes
6227399342). The pending.changeset/23858-uncreatable-directory-refusal.mdline "Not covered yet: the runtime state fileos servewrites intoOS_HOMEonce it is listening." is superseded by this PR's changeset. It is left in place because the finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR'sminorchangeset and every gate stayed green #17712 guard gates any edit of another PR's pending changeset on a person's confirmation.carrier:the release owner, atchangeset versiontime: drop that line from the compiled entry.catch { /* non-fatal — supervision file is best-effort */ }around the write prints nothing. Measured on this branch: underOS_HOME=/proc/os-23858-r2and underOS_HOME=SCRATCH/afile/sub, whereafileis a regular file (ENOTDIR),os serveprints its banner at about 4.6 s, and its output names neither the state file nor the error. A supervisor told "ready" then getsENOENTon the file. TheENOTDIRcase answered that way before this PR too. Before this PR the/proccase hung instead. The catch's posture is not this card's; it is in the report.dev.tsandstandalone-stack.tsare not in this diff (see The two folds).makeDirectorySync, both packages) and the lenient one (driver-sql'smakeDatabaseDirectoryand the two copies above).Generated by Claude Code