Skip to content

fix(cli): os dev compiles the config before it boots, so served connector metadata matches dispatch - #26240

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

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-dfvs2j

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #26130

Clause-②: no
Metadata-protocol: none

What was wrong

A host config's os dev boot holds two copies of one stack. The config module, evaluated in the serve child, feeds the AppPlugin wrap; provider-bound connectors are materialized from that registry copy, so it is what dispatch calls. The compiled dist/objectstack.json feeds the dev artifact door, and its copy is what /api/v1/meta/connector/NAME serves. examples/app-showcase builds its connectors' baseUrl from OS_PORT at module evaluation, and os dev booted whatever artifact was already on disk, so the two copies disagreed whenever the artifact had been built in another environment.

Measured on examples/app-showcase at this branch's base (c7aa9cde), OS_PORT=41302, the artifact compiled with no OS_PORT (baseUrl: http://127.0.0.1:3000), nothing listening on 3000. "dispatch" is POST /api/v1/automation/showcase_declarative_connector_ping/trigger, whose one node calls showcase_status_api.request for GET /api/v1/health.

boot GET /api/v1/meta/connector/showcase_status_api → providerConfig.baseUrl dispatch
os dev --no-compile (= a bare os dev before this PR) http://127.0.0.1:3000 200, status: ok (reached 41302)
os dev (this PR) http://127.0.0.1:41302 200, status: ok
os start (production, config joins the boot) http://127.0.0.1:41302 200, status: ok
os serve objectstack.config.ts (production) http://127.0.0.1:41302 200, status: ok
os serve --dev objectstack.config.ts http://127.0.0.1:3000 200, status: ok (reached 41302)

os start and production os serve compose no artifact door, so they serve the module's copy and already agree; they are unchanged. os serve --dev has the same two copies; see Acceptance notes.

The fix

Of the two routes the order named, this takes (b): the dev boot compiles instead of reading the artifact as found. --compile is now on by default, so os dev compiles objectstack.config.ts into the artifact it serves, in its own environment, before the serve child starts. Both copies are then one evaluation's answer, and nothing restates which variables a config reads or in what order (no second copy of the port/URL precedence). The watch loop's recompiles already ran in that environment, so the two stay one answer for the session.

Route (a), re-resolving env-derived connector config at an artifact boot, has nothing to re-resolve from: the artifact holds the value, not the expression that produced it. Carrying the expression would need a new packages/spec form for env-derived values, which the rulings put out of reach, so it is not proposed.

  • packages/cli/src/commands/dev.ts: the decision is one exported function, devCompilesAtBoot. A pinned artifact (--artifact, OS_ARTIFACT_URL, a remote OS_ARTIFACT_PATH) is never rebuilt. --no-compile keeps the old shortcut and the startup staleness warning. A missing artifact is still compiled. With no config, an existing artifact boots as before.
  • Cost: one compile per dev start. Measured on the showcase: 6.5 s for os compile (737 KB artifact).
  • content/docs/deployment/cli.mdx: the --compile row now says default on and names --no-compile. This file is outside the claim's file surface, but the docs row would be false without it.
  • scripts/check-cli-test-child-env.mjs: one census entry for the new test, which spawns the built entry (see Gates).
  • examples/app-showcase/src/system/self-url.ts: header comment only. Its os dev claim ("follows the live environment") is now true for the served metadata too, and it names the --no-compile exception.

Tests

At head ac8dadabc (the three dev test files rerun there: Test Files 3 passed (3), Tests 15 passed (15); the unit tier and typecheck ran at 7f3d5bee, whose only difference from the head is the census line in scripts/check-cli-test-child-env.mjs):

  • New packages/cli/test/dev-connector-env-parity.integration.test.ts (integration tier, built entry). It uses a minimal host config with one provider-bound connector whose baseUrl is built from OS_PORT the way self-url.ts builds it, and a host plugin whose provider factory records the providerConfig it was handed. A prebuilt artifact carries :3000. One os dev boot with OS_PORT=41302 checks that the served baseUrl and the materialized one both read http://127.0.0.1:41302. Result: Tests 2 passed (2).
  • New packages/cli/test/dev-compile-at-boot.pin.test.ts (unit tier) checks oclif's parse plus devCompilesAtBoot for each case: bare, --no-compile, pinned, missing artifact, no config. It passed with dev-no-watch.pin.test.ts.
  • pnpm --filter @objectstack/cli exec vitest run --project unit: Tests 1 failed | 4858 passed. The one failure was boot-preparation-parity.test.ts, which timed out at 5934 ms against its 5000 ms limit while the gate sweep shared the box. Run alone it answered Tests 37 passed (37). It reads serve.ts, which this PR does not touch.
  • pnpm --filter @objectstack/cli typecheck: exit 0.
  • Ablation, run once: devCompilesAtBoot's default arm was mutated to false through scripts/ablation-replace.mjs, the CLI was rebuilt, and ablation-dist-preflight found the marker in packages/cli/dist/commands/dev.js. The integration test then failed with expected 'http://127.0.0.1:3000' to be 'http://127.0.0.1:41302' on the served leg, while the materialized leg stayed green, which is the card's defect exactly. The source was restored (blob equals HEAD, git diff HEAD empty), the CLI rebuilt, and the preflight --absent check passed.

Gates

Local subset, node scripts/pm/dispatch-gates.mjs --commands at ac8dadabc: 120 families derived. --ran reconciliation: 118 run with exit 0, 2 NOT MEASURED, 0 unrun.

  • NOT MEASURED, reason: prerequisite not met (exit 3), because both read a full-repo build that this container did not make: pnpm --filter @objectstack/spec run check:skill-examples, pnpm check:dual-build-cjs-loads. CI runs both.
  • pnpm check:cli-test-child-env went red on the first sweep because its census pins the exact set of test files that spawn the built bin/run.js, and the new integration test joins that set. The census in scripts/check-cli-test-child-env.mjs now names the file with a one-line reason, the same form as the other members. The gate is green, and its self-test passed all 152 cases. The script's one other git grep hit is a comment in serve-node-env-production-default.e2e.test.ts, which no vitest project selects.
  • node scripts/check-plugin-teardown-shape.mjs --self-test exited 3 on the shallow clone. After git fetch origin 621a4876… (its positive control), it passed 48 cases.
  • node scripts/check-nul-bytes.mjs: exit 0. pnpm exec eslint --no-inline-config on the four touched source files: exit 0. Neither touched package has a lint script.
  • Every family CI runs is cited from this PR's check-runs in the report, not re-run here.

Acceptance notes

  • os serve --dev objectstack.config.ts has the same disagreement (row 5 above): it composes the dev artifact door and does not compile. This PR does not widen to it, per the ruling. It is reported to the seat.
  • os dev -p 41302 with no OS_PORT in the environment: os dev forwards the port to serve as --port and exports no OS_PORT, so the showcase's config resolves :3000 in both copies. They agree, but dispatch fails with connector_action(showcase_status_api.request) failed: fetch failed (measured, FLOW_FAILED 400). self-url.ts says it follows "the same names, in the same order, that the CLI itself reads to choose the listen port", and the flag is not one of them. This is Showcase REST connectors hard-wire baseUrl to http://127.0.0.1:3000, so self-ping flows fail fetch failed on any isolated instance — a port mismatch, not an egress block #7538's symptom on the flag path, not this card. It is reported to the seat.
  • os dev now rewrites dist/objectstack.json at every start. An os start that later boots that file alone serves the values of the last dev boot's environment. That was already true after any watch recompile, and self-url.ts already says to set the environment for the build.

#26088 remains open; this PR does not address it.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cj99V2CpVvyWDjQu1ndoBS

…ctor metadata matches dispatch

On a host config's os dev boot the served metadata (dev artifact door) and
the materialized connectors (the config module) are two copies; an
env-derived value differed between them whenever the artifact had been built
in another environment. Compile by default (--no-compile keeps the old
shortcut) so both come from one evaluation.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cj99V2CpVvyWDjQu1ndoBS
…s a query engine

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cj99V2CpVvyWDjQu1ndoBS
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 4 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via getTypes (sdk, the bare tail of client method meta.getTypes, bound to GET /api/v1/meta), meta.getTypes (sdk, the route ledger binds it to GET /api/v1/meta))
What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: os dev (command, 39 pages), /api/v1/meta (route, 42 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 df8aac41f2d9d14db04afdfe48511216419dafc0 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a0f194ae8e8af9283612f52005de2871440207b9 — the merge of head ac8dadabc591623d4329935c82a3714e4e253ce5 into base df8aac41f2d9d14db04afdfe48511216419dafc0, 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 a0f194ae8e8af9283612f52005de2871440207b9 && git checkout a0f194ae8e8af9283612f52005de2871440207b9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin df8aac41f2d9d14db04afdfe48511216419dafc0 ac8dadabc591623d4329935c82a3714e4e253ce5 && git checkout -B drift-repro df8aac41f2d9d14db04afdfe48511216419dafc0 && git merge --no-ff ac8dadabc591623d4329935c82a3714e4e253ce5

node scripts/docs-audit/affected-docs.mjs --json df8aac41f2d9d14db04afdfe48511216419dafc0

⚠️ 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 df8aac41f2d9d14db04afdfe48511216419dafc0 → 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-26T16:55Z · head ac8dadabc591623d4329935c82a3714e4e253ce5 · #26130

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

  1. Shape: a draft against main, Fixes #26130. Clause-②: no and Metadata-protocol: none are line-initial. One patch changeset for @objectstack/cli. check-governed-merges --pr 26240: NOT governed, 458 changed lines.
  2. Premise and review: the order asked for one resolution behind both the served metadata and dispatch, and the dev measured and took route (b): an os dev boot now compiles the config under its own environment by default (devCompilesAtBoot), so the artifact copy and the module copy of the stack are one evaluation's answer, with no second copy of the port precedence. --no-compile keeps the old shortcut, a missing artifact is still compiled, and a pinned artifact (--artifact, OS_ARTIFACT_URL, a remote OS_ARTIFACT_PATH) is never rebuilt; the unit pin covers each of those branches, and the integration test spawns the built CLI on the showcase with OS_PORT=41302 and reads the served baseUrl on that port (it read 3000 before). os start and production os serve already agreed and are unchanged. self-url.ts's header and the --compile row of the CLI docs now say what the code does. The claim was amended (6421568197) for the docs row and the one census entry the spawning test needs.
  3. Gates: at this stamp the head's 48 check-runs read 38 success and 10 skipped (path-filtered), with none failing or in progress; mergeable, clean. The dev reports 118 of 120 derived gate families green locally; the two not measured need a full build and are CI's.
  4. Accepted deviations: the docs row and census entry outside the first claim's surface, as amended.
  5. Out of scope: filed as 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 (os serve --dev keeps the same split on its door) and cli/dev: os dev -p N without OS_PORT leaves the showcase self URL at 3000, so its self-ping connector fails fetch failed and /meta serves :3000 (the #7538 symptom on the port-flag path) #26243 (os dev -p N without OS_PORT leaves the showcase self URL at 3000).
  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: on an os dev boot with OS_PORT set, served connector metadata reads the build-time baseUrl (http://127.0.0.1:3000) while dispatch reaches the live port, so /meta disagrees with what runs #26130, which the merge closes.

Generated by Claude Code

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