Skip to content

fix(cli): os dev shifts off a busy port before it compiles, so the config reads the port the server binds - #26330

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-k8l0qs
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-k8l0qs

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread
Fixes #26270

Clause-②: no
Metadata-protocol: none

What changed

When os dev shifts off a busy port, the config now reads the port the server binds, through the one port channel #26243 set up (portFlagEnv, OS_PORT/PORT). No second copy of the precedence: the requested port is the expression dev.ts already had (port ?? '3000'), the walk is serve.ts's own getAvailablePort, and the notice is serve.ts's own wording, extracted into formatDevPortShiftNotice.

  • os dev (packages/cli/src/commands/dev.ts). Before the compile (the first process that evaluates the config), the parent runs the same walk from the same requested port. On a shift it prints the shift notice (stderr), exports the bound port with portFlagEnv (so the compile, the watch recompiles and the serve child read it), and forwards it as the child's --port, so the child binds it without walking again and a restart keeps it. An exhausted walk is left to the child, which repeats it and prints formatExhaustedPortSearchNotice as before.
  • os serve (packages/cli/src/commands/serve.ts). The development auto-shift branch now exports the port it shifted to with portFlagEnv, before loadProjectEnvFiles, the config load, and the direct os serve --dev boot compile (cli/serve: os serve --dev with OS_PORT set still serves the build-time connector baseUrl from /meta while dispatch reaches the live port (the #26130 split on the serve door) #26242). That is the same branch whether it was entered through --dev or NODE_ENV=development.
  • utils/port-contract.ts: portFlagEnv's caller list names the shift. examples/app-showcase/src/system/self-url.ts is unchanged: its header claims stay true.

Measured at the public door (examples/app-showcase, a TCP listener holding the requested port)

boot main (after #26268) this branch
os dev --fresh -p 41310 shift notice 41310 → 41311; /api/v1/meta/connector/showcase_status_api baseUrl :41310; POST /api/v1/automation/showcase_declarative_connector_ping/trigger 400 FLOW_FAILED (41330 → 41331, 41400 → 41401 after the main merge) baseUrl :41331 / :41401; trigger 200, ping.body.data.status: "ok"
OS_PORT=41320 os dev --fresh (no flag) 41320 → 41321; baseUrl :41320; trigger 400 (41340 → 41341) baseUrl :41341; trigger 200
os serve --dev -p 41370 41370 → 41371; trigger 400 (41350 → 41351) trigger 200; after merging #26289: (41390 → 41391) baseUrl :41391, trigger 200
NODE_ENV=development os serve -p 41380 41380 → 41381; trigger 400 (41360 → 41361) trigger 200

The main column's failure text is connector_action(showcase_status_api.request) failed: resilientFetch: request timed out after 30000ms because the probe's holder accepts and never answers; with nothing answering, the card measured fetch failed. The ruling's scope note on os serve without --dev: it does shift, under NODE_ENV=development only (portAutoShiftAllowed), and it is the same branch through the same channel, so it is fixed by the same line. Production os serve still refuses a busy port, unchanged.

The pin

packages/cli/test/dev-connector-env-parity.integration.test.ts gains a third boot: os dev -p N with N held for the whole boot by an HTTP neighbour answering 503. The bound port is read from the child's own banner. Three arms: the materialized connector baseUrl, the served /api/v1/meta/connector/NAME baseUrl, and a flow pinging /api/v1/health through the connector (fxp_ping, the fixture's showcase_declarative_connector_ping) answering 200.

Tests and gates (head 42bd366)

  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: lock VERDICT command-exit 0, Test Files 334 passed, Tests 4884 passed, at 42bd366 (4878 before the main merge, at 4cb7cd3).
  • --project integration on the two parity files: 2 files, 9 tests passed.
  • pnpm --filter @objectstack/cli typecheck (tsc plus check:test-typecheck, whose project lists this test file): exit 0.
  • ESLint --no-inline-config on the four touched sources: exit 0.
  • dispatch-gates --commands: 69 families, all run, --ran reconciliation 69 derived … 68 run, 1 NOT-MEASURED. Every run exited 0 except: check:dual-build-cjs-loads exit 3 (Run pnpm build first, NOT MEASURED, CI builds and runs it), and check-plugin-teardown-shape --self-test exit 3 on the shallow checkout, 0 after git fetch origin 621a4876 (its pinned fixture).

Acceptance notes

  • Residual window. The parent's probe releases the port before the child binds it. If something takes it in between, the child shifts once more, says so, and exports its own port for its config, but the artifact the parent compiled (/api/v1/meta) keeps the probed port until the next recompile. Stated in the code and the changeset.
  • --port text notice. When the parent shifts, the child receives the bound port as a number, so serve's portTextReadNotice (for text like 08080 read as 8080) does not print on that boot. It still prints whenever no shift happens.
  • Test instrument. holdPort() in test/helpers/serve-process.ts cannot be released once something has connected to it (CLOSE_WAIT, server.close() never calls back). Not filed: no product defect, no reach outside tests. Carrier: none.

Generated by Claude Code

… pings on the shifted port

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQ4sTujBVMFFGmdXcRmdd5
…ts the shifted port, so the config reads the port the server binds

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQ4sTujBVMFFGmdXcRmdd5
… the ping fails fast and the holder releases

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQ4sTujBVMFFGmdXcRmdd5
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 3 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/cli/src/utils/port-contract.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/backup-restore.mdx (via OS_PORT (literal, a string literal in Dev))
  • content/docs/deployment/cli.mdx (via OS_PORT (literal, a string literal in Dev))
  • content/docs/deployment/environment-variables.mdx (via OS_PORT (literal, a string literal in Dev))
  • content/docs/deployment/self-hosting.mdx (via OS_PORT (literal, a string literal in Dev))
  • content/docs/deployment/single-project-mode.mdx (via OS_PORT (literal, a string literal in Dev))
  • content/docs/getting-started/index.mdx (via OS_PORT (literal, a string literal in Dev))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via OS_PORT (literal, a string literal in Dev))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/cli/src/utils/port-contract.ts) — pages documenting those are invisible to this run
  • 2 anchor(s) matched too much of the corpus to be a work list: os dev (command, 39 pages), os serve (command, 40 pages)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 31 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 46255b19842480a4f5e4a2556ae9f2e0764c9943 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d61a1d74969fe4823beca10485765b46f0bdfdfd — the merge of head 42bd366894ee086ac41f19765db65e53372102e2 into base 46255b19842480a4f5e4a2556ae9f2e0764c9943, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d61a1d74969fe4823beca10485765b46f0bdfdfd && git checkout d61a1d74969fe4823beca10485765b46f0bdfdfd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 46255b19842480a4f5e4a2556ae9f2e0764c9943 42bd366894ee086ac41f19765db65e53372102e2 && git checkout -B drift-repro 46255b19842480a4f5e4a2556ae9f2e0764c9943 && git merge --no-ff 42bd366894ee086ac41f19765db65e53372102e2

node scripts/docs-audit/affected-docs.mjs --json 46255b19842480a4f5e4a2556ae9f2e0764c9943

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 46255b19842480a4f5e4a2556ae9f2e0764c9943 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-26T21:33Z · head 42bd366894ee086ac41f19765db65e53372102e2 · #26270

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape: a draft against main with Fixes #26270. Clause-②: no and Metadata-protocol: none are line-initial. One patch changeset on @objectstack/cli. check-governed-merges --pr 26330: NOT governed, 259 changed lines. main is merged in with a merge commit after fix(cli): a direct os serve --dev compiles the config before its dev artifact door serves it #26289 landed.
  2. Premise and review: triage 6424678120 and the claim 6425309639 asked that a dev boot shifting off a busy port have its config read the bound port, through the one port source. The seat read the diff.
    • os dev parent. It runs serve's own getAvailablePort walk from the requested port before its compile, the first process that evaluates the config. On a shift it prints serve's notice, now one function (formatDevPortShiftNotice) that both processes call, on stderr. It exports the bound port through portFlagEnv, the channel PR fix(cli): os dev -p and os serve --port export the port as OS_PORT/PORT, so a config reads the port the server listens on #26268 set up, and forwards it as the child's --port. The child's walk then finds that port free and does not shift again, and every restart keeps it.
    • serve's shift branch. It exports through portFlagEnv too, before the config load. That covers a direct os serve --dev and NODE_ENV=development os serve.
    • Nothing restates the precedence. Production os serve never reaches either branch, and -p 0 probes free, so it does not shift.
    • The dev's evidence. The new third boot in dev-connector-env-parity.integration.test.ts reads red 3 of 6 on the pre-fix build and green 6 of 6. The showcase ping goes from 400 to 200 for -p, OS_PORT, serve --dev and NODE_ENV=development serve.
  3. Gates: at this stamp the head's 47 check-runs read 36 success and 11 skipped (path-filtered UI, docs-build, dogfood matrix placeholders and opt-in harnesses), with none failing or in progress; mergeable, clean. Locally the dev reports 68 of 69 derived gate families exit 0, with dual-build-cjs-loads left to CI, which is green.
  4. Accepted deviations: one window is disclosed in the code and the changeset. A port taken between the parent's probe and the child's bind makes the child shift once more. The child says so and exports its own port for the config it loads, but /api/v1/meta keeps the probed port until the next recompile. Closing that race would need the parent to hold the socket, which is out of this card's scope.
  5. Out of scope: none filed.
  6. Landing: this seat runs pr_ready and then automerge_enable through the relay, confirms MERGED on both readings, and records the landing on cli/dev: when the requested port is busy, os dev serves on N+1 but the config already read N, so the showcase self URL and /meta point at a port nothing serves and the self-ping fails #26270, which the merge closes.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 26, 2026 21:35
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 26, 2026 21:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 583b024 Oct 26, 2026
48 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-k8l0qs branch October 26, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants