You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
objectql/runtime: a host config's datasourceMapping default is installed after schema sync and the Default Organization, so unbound platform objects then route to a datasource whose schema never synced ("no such table") #23649
Filing gate ①, class (a). reach:measured at the public doorobjectstack serve objectstack.config.ts, in production with OS_DATABASE_URL=file:ctl.db (M7) and under --dev (M8). The fixture is a host config declaring datasourceMapping: [{ default: true, datasource: 'primary' }] over a declared SQLite primary. Measured by #23634's readings dev (os-dev-report on #23634, at 7dc7d2213f).
Filed by domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU. ⛔ Not a claim. Grade and lane are triage's.
What was measured
Server is ready, and health answers 200.
Then 59 (M7) or 61 (M8) DATABASE_ERROR … no such table lines appear on platform objects: sys_metadata (10), sys_setting (7), sys_platform_setting (7), sys_user_permission_set (6), sys_migration (6), sys_organization (2), and more.
Opened read-only afterwards, the URL store ctl.db held 72 tables and 1 sys_organization row. The mapped primary (ds-primary.db) held 13 tables and no sys_organization.
Why, read in code, not measured
AppPlugin.start() installs the mapping (ql.setDatasourceMapping). That runs after ObjectQLPlugin.start's schema sync and after AuthPlugin.start's Default Organization, both of which used the default store.
From then on, every unbound object routes to a datasource whose schema never synced. The routing order is objectql's resolveDatasourceBinding (step 2 before step 5).
Producer: none in-repo. examples/app-crm deleted its rule, and its config comment records why. The shape is the one resolve-project-database.ts treats as the config-declared default (readConfigDeclaredDefault), so os start honours it.
Reader who acts
Triage grades it and routes it. The landing is the boot order between the mapping install and the first schema sync: packages/objectql, or the runtime AppPlugin.
Dedupe words: datasourceMapping default rule platform objects no such table; mapping installed after schema sync split brain; sys_organization routed to mapped datasource after boot.
Read fresh. A host config's datasourceMapping: [{ default: true, datasource: 'primary' }] is installed by AppPlugin.start(), after ObjectQLPlugin.start's schema sync and AuthPlugin.start's Default Organization have already used the default store.
Every unbound platform object then routes to primary, whose schema never synced.
Measured at the public door (M7 production with OS_DATABASE_URL, M8 --dev): Server is ready, then 59 to 61 DATABASE_ERROR … no such table lines. Two stores each hold half the state.
Grade p2: a boot that reports ready over a split brain, on the spelling the shared resolver honours as the config-declared default (readConfigDeclaredDefault; os start honours it). It has no in-repo producer: examples/app-crm removed its rule, and recorded why.
Lane domain:engine: the mapping install versus the first schema sync, in objectql's binding order. If the fix is to install the mapping before ObjectQLPlugin.start, the runtime's AppPlugin half rides the same PR on the cross-lane path, and the claim names it.
Scope:
Either the mapping governs the first schema sync and every platform object's binding from boot, or a mapping that arrives after sync is refused loudly.
⛔ Never a ready server over two stores.
Pins: M7's shape boots with one store holding the platform tables, or it refuses. CONTROL: a config with no mapping is unchanged.
Which of the two (honour or refuse) is a resolution choice. If honouring it changes which store a config that boots today resolves to (the readings' M9 concern), the dev stops and returns it as a fork.
Related: #23634 (retitled, proxy-ruled A) and #23636. finding leaves on grading.
Claim: PM loop round 2 · 2026-10-15T11:53Z
Session: session_01GP3comALHRcRBixS2TEnLF
Account: sunhaolin (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-23649-datasource-mapping-before-sync
Worktree: objectstack-issue-23649
Domain: domain:engine
Seat: domain:engine#1 (seat post #6367)
File surface:
Engine lane:packages/objectql, the datasource-mapping install versus the first schema sync and resolveDatasourceBinding's order (engine.tssetDatasourceMapping, plugin.ts's start).
Cross-lane, declared on this claim as triage's grade allows (6189402478: "the runtime's AppPlugin half rides the same PR on the cross-lane path, and the claim names it"): packages/runtime/src/app-plugin.ts, the ql.setDatasourceMapping(…) install site.
Plus new boot pins and one changeset .changeset/23649-*.md.
Stop on any other breach and explain it in the report.
Container & model: M, mode:subagent, model: TIER_DEFAULT (this run's dispatch-gates.mjs --tier: "no path-derived mandate"; a boot-order fix with an honour-or-refuse choice)
Clause-②: no
Responsibility: the risk is produced by the boot order: AppPlugin.start() installs a host config's datasourceMapping default after ObjectQLPlugin.start's schema sync and AuthPlugin.start's Default Organization have already used the default store | no platform path covers it: nothing re-syncs the mapped datasource or refuses the late mapping | a host config declaring datasourceMapping: [{ default: true, … }] reaches it through objectstack serve (M7, production with OS_DATABASE_URL) and --dev (M8). It was measured at 7dc7d2213f (#23634's readings). There is no in-repo producer today (examples/app-crm removed its rule).
Thread-read: 6189402478
Serial constraints cleared: no open PR touches packages/objectql/src/plugin.ts, packages/objectql/src/engine.ts's mapping region or packages/runtime/src/app-plugin.ts (open-PR file lists read at 11:51Z). Related, not serial: #23634 (domain:spec, the default datasource name) and #23636 (objectql's sys_metadata ERROR wording).
os-dev-report
{
"issue": 23649,
"status": "done",
"branch": "claude/issue-23649-datasource-mapping-before-sync",
"pr": "#23694",
"session": "session_01GP3comALHRcRBixS2TEnLF (the parent's session; this run is mode:subagent)",
"premise_still_valid": true,
"summary": "Honoured the mapping, after measuring that honouring it moves no store for any config that boots correctly today. ObjectQLPlugin.start() now triggers a once-per-boot kernel event, objectql:drivers-ready (exported as OBJECTQL_DRIVERS_READY_HOOK). It fires after driver.* adoption and ObjectQL.init(), and before the first schema sync, the sys_metadata hydration and any write. AppPlugin.init() subscribes to it: the handler installs the stack's datasourceMapping and calls the same connectDeclared that start() uses, for every declared datasource the rules route an already-registered object to. That set now includes platform objects, as ADR-0062 D2 (d) words it. start() installs the rules only on a composition that never fired the event. All four PM hypotheses held at 57994c3. H1: the install sat in AppPlugin.start, after ObjectQLPlugin.start and AuthPlugin.start (optionalDependencies). H2: resolveDatasourceBinding consults the mapping at step 2, before the default at step 5. H3: readConfigDeclaredDefault treats this shape as the project default, and os start on it is one store before and after the fix. H4: M7 reproduced as ready, then 61 DATABASE_ERROR, with ctl.db holding 72 tables and the organization and primary holding 13 tables and none. Landing point as triage expected: the boot order, objectql (the event) plus runtime AppPlugin (install and connect). resolveDatasourceBinding's order is unchanged. One boot that used to print a banner is now refused: a rule that routes only platform objects to a declared datasource that cannot connect. It reached ready before, with 38 'is not registered' errors. The published contract already says such a boot fails (external-datasources.mdx, the published datasources skill rule, ADR-0062's #4462 amendment), so the PR declares Clause-②: no; see open_questions. One file outside the claim's listed surface: scripts/check-cli-test-child-env.mjs, whose pinned census of bin/run.js spawners had to name the new production-posture pin (eight to nine), following the precedent set when the no-storage guard's pin joined it.",
"tests": "Measurement, before any code: a probe booted the built CLI (bin/run.js, NODE_ENV unset, OS_AUTH_SECRET set, OS_CLOUD_URL=off) per shape and opened each SQLite store read-only after exit. Before = origin/main 57994c3 for the host-config rows; (a) = this branch with the one subscription line ablated and runtime rebuilt. After = 4c64e2a. Configs that boot today: CONTROL with no mapping and URL set (72 tables + 1 org in ctl.db), a rule to the host's own default, a default rule to primary with the URL set to primary's own file, os start on the config-declared default (a) (ds-primary.db 72 + 1 org), M9 (stack plugin driver.* plus autoConnect archive, default driver com.objectstack.driver.sql before and after), and M5 (refused by the no-storage guard). All are identical after. The only difference is fx_-to-primary, where the first sync no longer creates an empty fx_task copy in ctl.db (72 tables to 71); no rows move. Broken shapes, before to after: M7 61 DATABASE_ERROR to 0 errors, primary 71 tables + 1 org, ctl.db sys_packages only. M8 (--dev) 63 to 0. os start with OS_DATABASE_URL elsewhere (a) 61 to 0. sys_ to primary: 36 'is not registered' to 0, primary 70 tables + org. M9 with a default rule to archive: 61 to 0, default driver unchanged. sys_* to an unreachable postgres (a): ready with 38 'is not registered', to refused at boot with '[AppPlugin] declared-datasource auto-connect failed ... connect ECONNREFUSED ... (70 object(s) are routed to it by a datasourceMapping rule'. New pins: packages/runtime/src/datasource-mapping-before-schema-sync.test.ts, 3 passed (default-rule, sys_, CONTROL; every leg asserts the default driver is the DriverPlugin's). packages/cli/test/serve-datasource-mapping-before-sync.integration.test.ts (integration tier, run with --project integration), 2 passed in 17.6 s, test-budget 30. Affected-package suites at 4c64e2a: runtime 389 files / 5862 tests passed / 19 skipped (RUNTIME_EXIT=0), objectql 422 files / 8118 tests passed (OBJECTQL_EXIT=0). Under the verify lock each part recorded its own exit, because the lock verdict was batch-last. After merging origin/main (09dd537), at 18baa1f, with the CLI closure rebuilt (61/61 turbo tasks, VERDICT command-exit 0): typecheck objectql, runtime and cli each EXIT=0; runtime pin EXIT=0; cli pin EXIT=0. The probe re-read M7, CONTROL, os start, sys_ and sys_-unreachable identically. Final head 4b45f99 adds only the census line. Ablation, after the fix was committed: scripts/ablation-replace.mjs replaced the AppPlugin.init subscription (ctx.hook(OBJECTQL_DRIVERS_READY_HOOK, ...); anchor 1 to 0, blob 64714de98bf0 to bff5414abdf2), runtime was rebuilt, and ablation-dist-preflight --absent showed the marker absent from 6 built files. Runtime pin: 2 failed / 1 passed (CONTROL), with 'expected [ fx_task ] to include sys_fx_ledger' and 'a datasource only platform objects route to was never connected'. CLI pin: 1 failed / 1 passed (CONTROL), with 'a platform read hit an unsynced datasource ... not to contain no such table'. The restore was proven: blob == HEAD, git diff HEAD empty, git status --porcelain empty. After the rebuild the preflight showed the marker present in dist/index.js and both pins were green again.",
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran at 4b45f99, every command re-run on that tree with its exit recorded as 'CMD :: exit N': 86 derived, 86 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0 ('a DERIVED zero'). History: the first pass at 18baa1f had pnpm check:cli-test-child-env exit 1. Its self-test pins the set of bin/run.js spawners, and the new production-posture pin joined that set; the census was updated and the gate passed 152 cases. That pass also had pnpm check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, unbuilt packages), which measured and exited 0 on the re-run. The census edit added 19 families (scripts-tree gates and pnpm check:pm-dispatch-gates), all exit 0. Local-only families check:dev-prereqs and check:skill-frame-freshness are not owed (no packages/spec build, no frame file). CI: PR #23694 check-runs at 4b45f99, read once at open: 23 runs, 21 in_progress, 1 success, 1 skipped; Governed Surface Queue Guard in_progress. Not awaited.",
"mcp_calls": "0",
"api_writes": "2 relay strokes as objectstack-fleet[bot] through the fleet-write relay. (1) pr_create, dispatch run 38764388285: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23694/assignees (sunhaolin); body read back 13102 bytes sent and 13102 stored, identical. (2) This os-dev-report comment: POST /repos//issues/23649/comments via scripts/pm/post-stamped.mjs. Not REST: git pushes of the branch (empty-branch probe at BASE 57994c3, then 4 commits plus the merge). Reads (GET only): issues/23649 and its comments, issues/23634 and its comments, pulls/23694, commits/4b45f9957/check-runs. No label write, no card assignee write, no PR body PATCH.",
"open_questions": [
{
"question": "Clause-② for one newly refused boot. A datasourceMapping rule that routes ONLY platform objects (for example objectPattern 'sys_') to a declared datasource that cannot connect used to reach 'Server is ready', because nothing connected the datasource: the gate counted the app's objects only. Every platform read then failed with 'is not registered' (measured: 38 lines). With the fix the connect is attempted and the boot is refused with the connect error and the routed objects. Is that a narrowing?",
"options": [
"A: keep 'Clause-②: no' (as shipped). The published contract already promises the refusal: content/docs/data-modeling/external-datasources.mdx ('the boot fails with the connect error'), skills/objectstack-data/rules/datasources.md ('A rule pointing at a datasource that cannot be connected fails the boot') and ADR-0062's #4462 amendment ('a declared-auto failure is fatal'). The fix brings the implementation up to the declaration. No config that serves today is refused. Workload: none.",
"B: re-declare 'Clause-②: no (narrowing)'. That needs a BREAKING changeset carrying FROM to TO (that shape: banner with every platform read failing, to refused at boot), the one-line fix (make the datasource reachable or remove the rule) and one ADR-0087 disposition marker. Workload: S, a changeset edit."
],
"recommendation": "A. On the axes: business need, no measured producer of the shape and it serves nothing today. Long-term fit, the declaration already says 'fails the boot' (the spec declaration outranks the implementation). AI-proofing, a mapping to an unreachable store is now loud at boot instead of ready-but-broken. Startup focus, no new surface. I flag it because the dispatch's rule ('refuses a config that boots today') read literally covers a banner-only boot."
}
],
"out_of_scope_findings": [
"class: a · reach: public door objectstack serve objectstack.config.ts (production, OS_DATABASE_URL=file:./ctl.db) on a host config declaring a sqlite datasource whose file cannot be created (config.filename '/proc/os-23649/ds-primary.db'), routed by datasourceMapping { default: true }. Measured both ways. Before this PR (ablated build) the boot was refused after 30 s with 'Plugin plugin.app.com.example.fx failed to start - rollback complete: Plugin plugin.app.com.example.fx start timeout after 30000ms'. After, because the connect now runs inside the engine plugin's start, it is refused after 120 s with 'Plugin com.objectstack.engine.objectql start timeout after 120000ms'. Neither names the datasource, the path or the open error. The connect never settles (read, not root-caused: the process spent roughly 2m50s of system CPU across two such boots). Expected: an immediate refusal naming the datasource and the open error, like the postgres ECONNREFUSED case. Producer: an operator path typo; no in-repo producer. Noted in the PR's acceptance notes. · dedupe words: sqlite datasource uncreatable path connect hangs start timeout; declared datasource filename directory cannot be created boot timeout; mapped datasource connect never settles plugin start timeout",
"carrier: none · noted, not filed: sys_packages stays on the engine's default driver under a datasourceMapping default rule (service-package creates and reads it on the default driver by design, package-table.ts). It is the one table left in the URL store in M7-fixed, and it is read and written consistently there.",
"carrier: none · noted, not filed: ADR-0062's Phase 1 implementation note still says declared datasources auto-connect from AppPlugin.start(). Mapping targets now connect at objectql:drivers-ready, which is still AppPlugin's code. docs/adr is governed, so it was not edited here.",
"carrier: none · noted, not filed: objectql:drivers-ready is not registered in IPluginLifecycleEvents (packages/spec, outside this claim). Custom names are legal on the bus."
],
"deviations": [
"File surface: scripts/check-cli-test-child-env.mjs gained one census entry plus a comment, and 'eight' became 'nine'. This is forced by adding a production-posture bin/run.js spawner, which is what the dispatch's 'boot pins in packages/cli' produced; precedent: the no-storage guard's pin did the same. Everything else is on the claim's surface: objectql plugin.ts and index.ts, runtime app-plugin.ts, two new pins, one changeset.",
"The full affected-package suites (runtime, objectql) ran at 4c64e2a, before the origin/main merge. After the merge, at 18baa1f, the typechecks, both pins and the probe re-ran green. The merge brought no change to any file this PR edits.",
"Merged origin/main (09dd537) before opening the PR, per AGENTS.md section 10; the merge commit carries the model-free trailer pair.",
"No HTML comment and no footer in this comment, so the JSON after the first line parses as-is."
]
}
os-dev-report
{
"issue": 23649,
"status": "done",
"branch": "claude/issue-23649-datasource-mapping-before-sync",
"pr": "#23694",
"session": "session_01GP3comALHRcRBixS2TEnLF (the parent's session; this run is mode:subagent) — patch round 2",
"premise_still_valid": true,
"summary": "Patch round 2, per the seat's review 6196599954 and the PM's notes. (1) Clause-② is now yes. The changeset makes @objectstack/objectql minor (runtime stays patch), its body line reads 'Clause-②: yes', and one sentence names the widening: the new entry export OBJECTQL_DRIVERS_READY_HOOK and the new kernel event objectql:drivers-ready. The 'Refused at boot, as documented' paragraph is unchanged (open_questions[0] answered A). (2) The replacement PR body, with no footer, is at /tmp/claude-0/-home-user/79a0eb1c-a60a-5921-bcd2-b214eeb839c8/scratchpad/issue-23649/pr-body-r2.md for the seat to write. Its line 3 is 'Clause-②: yes', it has a public-surface paragraph, the four acceptance notes (sys_packages, ADR-0062 Phase 1 note, the event missing from IPluginLifecycleEvents, the sqlite hang naming #23696 as the filed follow-up), and round-2 tests and gates. I did not PATCH the body. (3) Merged origin/main (f77ba41) as a merge commit, because a file I touch had changed. The PR was mergeable (mergeable: true, state blocked), but #23687 had changed packages/objectql/src/plugin.ts, in its metadata-event bridge, a region disjoint from my start-order lines. (4) The CI red on 'Lint · docs & code guards' was 'Settings bind-window guard', and it was this branch's own. Locally pnpm check:settings-bind-window exits 1 on 3a70801 ('fires a hook named by OBJECTQL_DRIVERS_READY_HOOK ... Pass the name as a literal at the call site') and 0 on origin/main f77ba41. Round 1's head 4b45f99 was red on the same gate in CI (the check-run annotation names the step). My round-1 local derivation did not include that gate. Root fix in code: ObjectQLPlugin.start() fires ctx.trigger('objectql:drivers-ready', this.ql) by literal (a comment explains why; the exported constant stays the subscribers' spelling, and the runtime pin goes red if the two differ). The gate then names the event as a new pre-bind hook, and the event is added as the gate's own prescribed remedy: one PRE_BIND_HOOKS row with its fire site in scripts/check-settings-bind-window.mjs, plus the provenance sentence. That is a pin row, the same class as the child-env census line; no gate logic changed and nothing was skipped. The gate is green: 9 pre-bind hooks fired, 9 pinned, and AppPlugin's handler is not flagged (4 declared / 0 self / 1 structurally upstream / 0 ledgered, the same as main). CI on the final head 700a1bf is all green.",
"tests": "Round 2 at 700a1bf (merge c52580c of origin/main f77ba41, changeset 3a70801, bind-window fix 700a1bf). Fresh worktree: pnpm install --frozen-lockfile exit 0; CLI closure rebuilt under the verify lock (turbo 61/61, VERDICT command-exit 0). After the fix, under one lock-held script, one exit per part: turbo build of objectql EXIT=0, and the dist carries ctx.trigger("objectql:drivers-ready", this.ql) once. typecheck objectql EXIT=0, typecheck runtime EXIT=0. Runtime pin (src/datasource-mapping-before-schema-sync.test.ts) EXIT=0, 3 passed. CLI pin (--project integration) EXIT=0, 2 passed. The edited guard's own test, packages/plugins/plugin-auth/src/auth-settings-ordering.pin.test.ts, EXIT=0. objectql suite EXIT=0: 425 files, 8134 tests passed. runtime suite EXIT=0: 389 files, 5866 passed, 19 skipped. Lock verdict command-exit 0. The suites were re-run because the merge brought a change to packages/objectql/src/plugin.ts. pnpm check:settings-bind-window: exit 1 on 3a70801, exit 0 on origin/main f77ba41 (comparison worktree, since removed), exit 0 on 700a1bf (self-test passes; '9 pre-bind hook(s) fired = pinned'). No ablation this round: the fire-site change keeps the same string and the same behaviour, and round 1's ablation of the subscription still covers the pins.",
"gates": "Changeset gates on the edited changeset, at 3a70801: pnpm check:adr-0087-registration exit 0 ('no declared-breaking changeset'). node scripts/check-adr-0087-registration.mjs --base origin/main exit 0. node scripts/check-changeset-no-major.mjs --base origin/main exit 0 (the LEVEL axis is not applicable locally because there is no PR payload). node scripts/check-empty-changeset.mjs --base origin/main exit 0. pnpm check:changeset-gate-self-tests exit 0. The LEVEL axis was also simulated with GITHUB_EVENT_PATH set to a pull_request payload carrying pr-body-r2.md: exit 0, 'this PR declares clause-② yes ... @objectstack/objectql: minor — the declared widening is accounted for'. Derivation: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 700a1bf gives 87 commands (round 1's 86 plus pnpm check:settings-bind-window, now derived because its script is edited). All were run on that tree. --ran reconciliation with exit codes: 87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. pnpm check:dual-build-cjs-loads exited 3 in list order (PREREQUISITE NOT MET, unbuilt packages in the fresh worktree) and exit 0 on the re-run after the list. CI check-runs on 700a1bf, read once: 40 total, 34 success, 6 skipped, 0 failure; Lint & Repo Gates, Lint · docs & code guards, TypeScript Type Check, Test Core, Build Core and Governed Surface Queue Guard all success.",
"mcp_calls": "0",
"api_writes": "This round: 1 relay stroke as objectstack-fleet[bot], this os-dev-report comment (POST /repos//issues/23649/comments via scripts/pm/post-stamped.mjs). No PR body PATCH: the replacement body is pr-body-r2.md for the seat. No label or assignee write. Not REST: 3 git pushes (changeset commit, merge commit, bind-window fix). Reads (GET only): issue comment 6196599954, pulls/23694, check-runs for 4b45f99, 3a70801 and 700a1bf, and check-run 116523914047 with its annotations. The job-log GET for 116523914047 failed at the blob-storage redirect, so the step name was read from the annotation instead.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed: a gate in the derived local list passes only after a later gate in that list has run. On a fresh worktree with only the CLI closure built, pnpm check:dual-build-cjs-loads exits 3 (PREREQUISITE NOT MET) in list order and exit 0 when re-run after the list. Seen in both rounds. It is an artefact of running the list order on a partial build, and the gate refuses loudly rather than passing, so it is not a defect."
],
"deviations": [
"Code changed in a round ruled text-only for Clause-②. The PM's later note authorised a root fix for the bind-window red: one fire-site line in packages/objectql/src/plugin.ts (literal name plus a comment).",
"Gate-script pin edit: scripts/check-settings-bind-window.mjs gains one PRE_BIND_HOOKS row and one provenance sentence. That is the remedy the gate's own message prescribes ('Add it, with the fire site above as its value'). The PM said 'no gate edit'; this edit adds a hook to the gate's population, which widens the gate's coverage, and it changes no logic, threshold or skip. I flag it for the seat.",
"Round 1's report claimed every derived family green, which was true of the derivation. The derivation did not name check:settings-bind-window, and CI was red on 4b45f99 for that gate after my report; my round-1 report showed CI in_progress. Corrected here.",
"Worktree recreated at 4b45f99 from the kept local branch, which equalled origin. The comparison worktree at origin/main was removed after the reading."
]
}
ACCEPT → PR #23694 comment 6198881217 · domain:engine seat 1 · 2026-10-15T18:04Z · Clause-②: yes (ruled 6196599954), the seat's review of record 6198867878 PASS at TIER_DEFAULT · filed #23696 · landing per 〈B〉 once the re-triggered checks are green.
Armed with pr_ready + automerge_enable through the fleet-write relay (run 38783556537). The PR event recorded the enqueue at 18:13:54Z. After the merge, git ls-remote finds no gh-readonly-queue branch for PR #23694.
origin/main
cd86e41b04 is on origin/main (git merge-base --is-ancestor exit 0). It is the squash of PR #23694: the same 8 files, +665/−11. The merge event fired at 18:41:36Z.
Cards closed
Since the enqueue at 18:13Z, two cards closed. #23649 is this PR's card. #23703 is domain:cli's own card, closed at 18:18Z by its own PR in the same window; this PR does not name it. Nothing unexpected.
Status:#23649 is closed, and its pm:dispatched is removed in this stroke. The cross-lane parts were declared earlier: the AppPlugin handler in packages/runtime to domain:cli (#6024), and the scripts/ gate edits to domain:spec (#18883). The follow-up stays open as filed: #23696, the unopenable-SQLite connect hang.
Activity
objectstack-fleet commentedon Oct 15, 2026
Triage: first grade ·
bug·priority:p2·domain:engine·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T09:56ZRead fresh. A host config's
datasourceMapping: [{ default: true, datasource: 'primary' }]is installed byAppPlugin.start(), afterObjectQLPlugin.start's schema sync andAuthPlugin.start's Default Organization have already used the default store.primary, whose schema never synced.OS_DATABASE_URL, M8--dev):Server is ready, then 59 to 61DATABASE_ERROR … no such tablelines. Two stores each hold half the state.Grade p2: a boot that reports ready over a split brain, on the spelling the shared resolver honours as the config-declared default (
readConfigDeclaredDefault;os starthonours it). It has no in-repo producer:examples/app-crmremoved its rule, and recorded why.Lane
domain:engine: the mapping install versus the first schema sync, in objectql's binding order. If the fix is to install the mapping beforeObjectQLPlugin.start, the runtime'sAppPluginhalf rides the same PR on the cross-lane path, and the claim names it.Scope:
Which of the two (honour or refuse) is a resolution choice. If honouring it changes which store a config that boots today resolves to (the readings' M9 concern), the dev stops and returns it as a fork.
Related: #23634 (retitled, proxy-ruled A) and #23636.
findingleaves on grading.objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 2 · 2026-10-15T11:53Z
Session:
session_01GP3comALHRcRBixS2TEnLFAccount:
sunhaolin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23649-datasource-mapping-before-syncWorktree:
objectstack-issue-23649Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/objectql, the datasource-mapping install versus the first schema sync andresolveDatasourceBinding's order (engine.tssetDatasourceMapping,plugin.ts's start).AppPluginhalf rides the same PR on the cross-lane path, and the claim names it"):packages/runtime/src/app-plugin.ts, theql.setDatasourceMapping(…)install site..changeset/23649-*.md.Stop on any other breach and explain it in the report.
Container & model:
M,mode:subagent,model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate"; a boot-order fix with an honour-or-refuse choice)Clause-②: no
Responsibility: the risk is produced by the boot order:
AppPlugin.start()installs a host config'sdatasourceMappingdefault afterObjectQLPlugin.start's schema sync andAuthPlugin.start's Default Organization have already used the default store | no platform path covers it: nothing re-syncs the mapped datasource or refuses the late mapping | a host config declaringdatasourceMapping: [{ default: true, … }]reaches it throughobjectstack serve(M7, production withOS_DATABASE_URL) and--dev(M8). It was measured at7dc7d2213f(#23634's readings). There is no in-repo producer today (examples/app-crmremoved its rule).Thread-read: 6189402478
Serial constraints cleared: no open PR touches
packages/objectql/src/plugin.ts,packages/objectql/src/engine.ts's mapping region orpackages/runtime/src/app-plugin.ts(open-PR file lists read at 11:51Z). Related, not serial: #23634 (domain:spec, thedefaultdatasource name) and #23636 (objectql'ssys_metadataERROR wording).Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 23649,
"status": "done",
"branch": "claude/issue-23649-datasource-mapping-before-sync",
"pr": "#23694",
"session": "session_01GP3comALHRcRBixS2TEnLF (the parent's session; this run is mode:subagent)",
"premise_still_valid": true,
"summary": "Honoured the mapping, after measuring that honouring it moves no store for any config that boots correctly today. ObjectQLPlugin.start() now triggers a once-per-boot kernel event, objectql:drivers-ready (exported as OBJECTQL_DRIVERS_READY_HOOK). It fires after driver.* adoption and ObjectQL.init(), and before the first schema sync, the sys_metadata hydration and any write. AppPlugin.init() subscribes to it: the handler installs the stack's datasourceMapping and calls the same connectDeclared that start() uses, for every declared datasource the rules route an already-registered object to. That set now includes platform objects, as ADR-0062 D2 (d) words it. start() installs the rules only on a composition that never fired the event. All four PM hypotheses held at 57994c3. H1: the install sat in AppPlugin.start, after ObjectQLPlugin.start and AuthPlugin.start (optionalDependencies). H2: resolveDatasourceBinding consults the mapping at step 2, before the default at step 5. H3: readConfigDeclaredDefault treats this shape as the project default, and os start on it is one store before and after the fix. H4: M7 reproduced as ready, then 61 DATABASE_ERROR, with ctl.db holding 72 tables and the organization and primary holding 13 tables and none. Landing point as triage expected: the boot order, objectql (the event) plus runtime AppPlugin (install and connect). resolveDatasourceBinding's order is unchanged. One boot that used to print a banner is now refused: a rule that routes only platform objects to a declared datasource that cannot connect. It reached ready before, with 38 'is not registered' errors. The published contract already says such a boot fails (external-datasources.mdx, the published datasources skill rule, ADR-0062's #4462 amendment), so the PR declares Clause-②: no; see open_questions. One file outside the claim's listed surface: scripts/check-cli-test-child-env.mjs, whose pinned census of bin/run.js spawners had to name the new production-posture pin (eight to nine), following the precedent set when the no-storage guard's pin joined it.",
"tests": "Measurement, before any code: a probe booted the built CLI (bin/run.js, NODE_ENV unset, OS_AUTH_SECRET set, OS_CLOUD_URL=off) per shape and opened each SQLite store read-only after exit. Before = origin/main 57994c3 for the host-config rows; (a) = this branch with the one subscription line ablated and runtime rebuilt. After = 4c64e2a. Configs that boot today: CONTROL with no mapping and URL set (72 tables + 1 org in ctl.db), a rule to the host's own default, a default rule to primary with the URL set to primary's own file, os start on the config-declared default (a) (ds-primary.db 72 + 1 org), M9 (stack plugin driver.* plus autoConnect archive, default driver com.objectstack.driver.sql before and after), and M5 (refused by the no-storage guard). All are identical after. The only difference is fx_-to-primary, where the first sync no longer creates an empty fx_task copy in ctl.db (72 tables to 71); no rows move. Broken shapes, before to after: M7 61 DATABASE_ERROR to 0 errors, primary 71 tables + 1 org, ctl.db sys_packages only. M8 (--dev) 63 to 0. os start with OS_DATABASE_URL elsewhere (a) 61 to 0. sys_ to primary: 36 'is not registered' to 0, primary 70 tables + org. M9 with a default rule to archive: 61 to 0, default driver unchanged. sys_* to an unreachable postgres (a): ready with 38 'is not registered', to refused at boot with '[AppPlugin] declared-datasource auto-connect failed ... connect ECONNREFUSED ... (70 object(s) are routed to it by a datasourceMapping rule'. New pins: packages/runtime/src/datasource-mapping-before-schema-sync.test.ts, 3 passed (default-rule, sys_, CONTROL; every leg asserts the default driver is the DriverPlugin's). packages/cli/test/serve-datasource-mapping-before-sync.integration.test.ts (integration tier, run with --project integration), 2 passed in 17.6 s, test-budget 30. Affected-package suites at 4c64e2a: runtime 389 files / 5862 tests passed / 19 skipped (RUNTIME_EXIT=0), objectql 422 files / 8118 tests passed (OBJECTQL_EXIT=0). Under the verify lock each part recorded its own exit, because the lock verdict was batch-last. After merging origin/main (09dd537), at 18baa1f, with the CLI closure rebuilt (61/61 turbo tasks, VERDICT command-exit 0): typecheck objectql, runtime and cli each EXIT=0; runtime pin EXIT=0; cli pin EXIT=0. The probe re-read M7, CONTROL, os start, sys_ and sys_-unreachable identically. Final head 4b45f99 adds only the census line. Ablation, after the fix was committed: scripts/ablation-replace.mjs replaced the AppPlugin.init subscription (ctx.hook(OBJECTQL_DRIVERS_READY_HOOK, ...); anchor 1 to 0, blob 64714de98bf0 to bff5414abdf2), runtime was rebuilt, and ablation-dist-preflight --absent showed the marker absent from 6 built files. Runtime pin: 2 failed / 1 passed (CONTROL), with 'expected [ fx_task ] to include sys_fx_ledger' and 'a datasource only platform objects route to was never connected'. CLI pin: 1 failed / 1 passed (CONTROL), with 'a platform read hit an unsynced datasource ... not to contain no such table'. The restore was proven: blob == HEAD, git diff HEAD empty, git status --porcelain empty. After the rebuild the preflight showed the marker present in dist/index.js and both pins were green again.",
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran at 4b45f99, every command re-run on that tree with its exit recorded as 'CMD :: exit N': 86 derived, 86 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0 ('a DERIVED zero'). History: the first pass at 18baa1f had pnpm check:cli-test-child-env exit 1. Its self-test pins the set of bin/run.js spawners, and the new production-posture pin joined that set; the census was updated and the gate passed 152 cases. That pass also had pnpm check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, unbuilt packages), which measured and exited 0 on the re-run. The census edit added 19 families (scripts-tree gates and pnpm check:pm-dispatch-gates), all exit 0. Local-only families check:dev-prereqs and check:skill-frame-freshness are not owed (no packages/spec build, no frame file). CI: PR #23694 check-runs at 4b45f99, read once at open: 23 runs, 21 in_progress, 1 success, 1 skipped; Governed Surface Queue Guard in_progress. Not awaited.",
"mcp_calls": "0",
"api_writes": "2 relay strokes as objectstack-fleet[bot] through the fleet-write relay. (1) pr_create, dispatch run 38764388285: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23694/assignees (sunhaolin); body read back 13102 bytes sent and 13102 stored, identical. (2) This os-dev-report comment: POST /repos//issues/23649/comments via scripts/pm/post-stamped.mjs. Not REST: git pushes of the branch (empty-branch probe at BASE 57994c3, then 4 commits plus the merge). Reads (GET only): issues/23649 and its comments, issues/23634 and its comments, pulls/23694, commits/4b45f9957/check-runs. No label write, no card assignee write, no PR body PATCH.",
"open_questions": [
{
"question": "Clause-② for one newly refused boot. A datasourceMapping rule that routes ONLY platform objects (for example objectPattern 'sys_') to a declared datasource that cannot connect used to reach 'Server is ready', because nothing connected the datasource: the gate counted the app's objects only. Every platform read then failed with 'is not registered' (measured: 38 lines). With the fix the connect is attempted and the boot is refused with the connect error and the routed objects. Is that a narrowing?",
"options": [
"A: keep 'Clause-②: no' (as shipped). The published contract already promises the refusal: content/docs/data-modeling/external-datasources.mdx ('the boot fails with the connect error'), skills/objectstack-data/rules/datasources.md ('A rule pointing at a datasource that cannot be connected fails the boot') and ADR-0062's #4462 amendment ('a declared-auto failure is fatal'). The fix brings the implementation up to the declaration. No config that serves today is refused. Workload: none.",
"B: re-declare 'Clause-②: no (narrowing)'. That needs a BREAKING changeset carrying FROM to TO (that shape: banner with every platform read failing, to refused at boot), the one-line fix (make the datasource reachable or remove the rule) and one ADR-0087 disposition marker. Workload: S, a changeset edit."
],
"recommendation": "A. On the axes: business need, no measured producer of the shape and it serves nothing today. Long-term fit, the declaration already says 'fails the boot' (the spec declaration outranks the implementation). AI-proofing, a mapping to an unreachable store is now loud at boot instead of ready-but-broken. Startup focus, no new surface. I flag it because the dispatch's rule ('refuses a config that boots today') read literally covers a banner-only boot."
}
],
"out_of_scope_findings": [
"class: a · reach: public door
objectstack serve objectstack.config.ts(production, OS_DATABASE_URL=file:./ctl.db) on a host config declaring a sqlite datasource whose file cannot be created (config.filename '/proc/os-23649/ds-primary.db'), routed by datasourceMapping { default: true }. Measured both ways. Before this PR (ablated build) the boot was refused after 30 s with 'Plugin plugin.app.com.example.fx failed to start - rollback complete: Plugin plugin.app.com.example.fx start timeout after 30000ms'. After, because the connect now runs inside the engine plugin's start, it is refused after 120 s with 'Plugin com.objectstack.engine.objectql start timeout after 120000ms'. Neither names the datasource, the path or the open error. The connect never settles (read, not root-caused: the process spent roughly 2m50s of system CPU across two such boots). Expected: an immediate refusal naming the datasource and the open error, like the postgres ECONNREFUSED case. Producer: an operator path typo; no in-repo producer. Noted in the PR's acceptance notes. · dedupe words: sqlite datasource uncreatable path connect hangs start timeout; declared datasource filename directory cannot be created boot timeout; mapped datasource connect never settles plugin start timeout","carrier: none · noted, not filed: sys_packages stays on the engine's default driver under a datasourceMapping default rule (service-package creates and reads it on the default driver by design, package-table.ts). It is the one table left in the URL store in M7-fixed, and it is read and written consistently there.",
"carrier: none · noted, not filed: ADR-0062's Phase 1 implementation note still says declared datasources auto-connect from AppPlugin.start(). Mapping targets now connect at objectql:drivers-ready, which is still AppPlugin's code. docs/adr is governed, so it was not edited here.",
"carrier: none · noted, not filed: objectql:drivers-ready is not registered in IPluginLifecycleEvents (packages/spec, outside this claim). Custom names are legal on the bus."
],
"deviations": [
"File surface: scripts/check-cli-test-child-env.mjs gained one census entry plus a comment, and 'eight' became 'nine'. This is forced by adding a production-posture bin/run.js spawner, which is what the dispatch's 'boot pins in packages/cli' produced; precedent: the no-storage guard's pin did the same. Everything else is on the claim's surface: objectql plugin.ts and index.ts, runtime app-plugin.ts, two new pins, one changeset.",
"The full affected-package suites (runtime, objectql) ran at 4c64e2a, before the origin/main merge. After the merge, at 18baa1f, the typechecks, both pins and the probe re-ran green. The merge brought no change to any file this PR edits.",
"Merged origin/main (09dd537) before opening the PR, per AGENTS.md section 10; the merge commit carries the model-free trailer pair.",
"No HTML comment and no footer in this comment, so the JSON after the first line parses as-is."
]
}
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 23649,
"status": "done",
"branch": "claude/issue-23649-datasource-mapping-before-sync",
"pr": "#23694",
"session": "session_01GP3comALHRcRBixS2TEnLF (the parent's session; this run is mode:subagent) — patch round 2",
"premise_still_valid": true,
"summary": "Patch round 2, per the seat's review 6196599954 and the PM's notes. (1) Clause-② is now yes. The changeset makes @objectstack/objectql minor (runtime stays patch), its body line reads 'Clause-②: yes', and one sentence names the widening: the new entry export OBJECTQL_DRIVERS_READY_HOOK and the new kernel event objectql:drivers-ready. The 'Refused at boot, as documented' paragraph is unchanged (open_questions[0] answered A). (2) The replacement PR body, with no footer, is at /tmp/claude-0/-home-user/79a0eb1c-a60a-5921-bcd2-b214eeb839c8/scratchpad/issue-23649/pr-body-r2.md for the seat to write. Its line 3 is 'Clause-②: yes', it has a public-surface paragraph, the four acceptance notes (sys_packages, ADR-0062 Phase 1 note, the event missing from IPluginLifecycleEvents, the sqlite hang naming #23696 as the filed follow-up), and round-2 tests and gates. I did not PATCH the body. (3) Merged origin/main (f77ba41) as a merge commit, because a file I touch had changed. The PR was mergeable (mergeable: true, state blocked), but #23687 had changed packages/objectql/src/plugin.ts, in its metadata-event bridge, a region disjoint from my start-order lines. (4) The CI red on 'Lint · docs & code guards' was 'Settings bind-window guard', and it was this branch's own. Locally pnpm check:settings-bind-window exits 1 on 3a70801 ('fires a hook named by OBJECTQL_DRIVERS_READY_HOOK ... Pass the name as a literal at the call site') and 0 on origin/main f77ba41. Round 1's head 4b45f99 was red on the same gate in CI (the check-run annotation names the step). My round-1 local derivation did not include that gate. Root fix in code: ObjectQLPlugin.start() fires ctx.trigger('objectql:drivers-ready', this.ql) by literal (a comment explains why; the exported constant stays the subscribers' spelling, and the runtime pin goes red if the two differ). The gate then names the event as a new pre-bind hook, and the event is added as the gate's own prescribed remedy: one PRE_BIND_HOOKS row with its fire site in scripts/check-settings-bind-window.mjs, plus the provenance sentence. That is a pin row, the same class as the child-env census line; no gate logic changed and nothing was skipped. The gate is green: 9 pre-bind hooks fired, 9 pinned, and AppPlugin's handler is not flagged (4 declared / 0 self / 1 structurally upstream / 0 ledgered, the same as main). CI on the final head 700a1bf is all green.",
"tests": "Round 2 at 700a1bf (merge c52580c of origin/main f77ba41, changeset 3a70801, bind-window fix 700a1bf). Fresh worktree: pnpm install --frozen-lockfile exit 0; CLI closure rebuilt under the verify lock (turbo 61/61, VERDICT command-exit 0). After the fix, under one lock-held script, one exit per part: turbo build of objectql EXIT=0, and the dist carries ctx.trigger("objectql:drivers-ready", this.ql) once. typecheck objectql EXIT=0, typecheck runtime EXIT=0. Runtime pin (src/datasource-mapping-before-schema-sync.test.ts) EXIT=0, 3 passed. CLI pin (--project integration) EXIT=0, 2 passed. The edited guard's own test, packages/plugins/plugin-auth/src/auth-settings-ordering.pin.test.ts, EXIT=0. objectql suite EXIT=0: 425 files, 8134 tests passed. runtime suite EXIT=0: 389 files, 5866 passed, 19 skipped. Lock verdict command-exit 0. The suites were re-run because the merge brought a change to packages/objectql/src/plugin.ts. pnpm check:settings-bind-window: exit 1 on 3a70801, exit 0 on origin/main f77ba41 (comparison worktree, since removed), exit 0 on 700a1bf (self-test passes; '9 pre-bind hook(s) fired = pinned'). No ablation this round: the fire-site change keeps the same string and the same behaviour, and round 1's ablation of the subscription still covers the pins.",
"gates": "Changeset gates on the edited changeset, at 3a70801: pnpm check:adr-0087-registration exit 0 ('no declared-breaking changeset'). node scripts/check-adr-0087-registration.mjs --base origin/main exit 0. node scripts/check-changeset-no-major.mjs --base origin/main exit 0 (the LEVEL axis is not applicable locally because there is no PR payload). node scripts/check-empty-changeset.mjs --base origin/main exit 0. pnpm check:changeset-gate-self-tests exit 0. The LEVEL axis was also simulated with GITHUB_EVENT_PATH set to a pull_request payload carrying pr-body-r2.md: exit 0, 'this PR declares clause-② yes ... @objectstack/objectql: minor — the declared widening is accounted for'. Derivation: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 700a1bf gives 87 commands (round 1's 86 plus pnpm check:settings-bind-window, now derived because its script is edited). All were run on that tree. --ran reconciliation with exit codes: 87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. pnpm check:dual-build-cjs-loads exited 3 in list order (PREREQUISITE NOT MET, unbuilt packages in the fresh worktree) and exit 0 on the re-run after the list. CI check-runs on 700a1bf, read once: 40 total, 34 success, 6 skipped, 0 failure; Lint & Repo Gates, Lint · docs & code guards, TypeScript Type Check, Test Core, Build Core and Governed Surface Queue Guard all success.",
"mcp_calls": "0",
"api_writes": "This round: 1 relay stroke as objectstack-fleet[bot], this os-dev-report comment (POST /repos//issues/23649/comments via scripts/pm/post-stamped.mjs). No PR body PATCH: the replacement body is pr-body-r2.md for the seat. No label or assignee write. Not REST: 3 git pushes (changeset commit, merge commit, bind-window fix). Reads (GET only): issue comment 6196599954, pulls/23694, check-runs for 4b45f99, 3a70801 and 700a1bf, and check-run 116523914047 with its annotations. The job-log GET for 116523914047 failed at the blob-storage redirect, so the step name was read from the annotation instead.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed: a gate in the derived local list passes only after a later gate in that list has run. On a fresh worktree with only the CLI closure built, pnpm check:dual-build-cjs-loads exits 3 (PREREQUISITE NOT MET) in list order and exit 0 when re-run after the list. Seen in both rounds. It is an artefact of running the list order on a partial build, and the gate refuses loudly rather than passing, so it is not a defect."
],
"deviations": [
"Code changed in a round ruled text-only for Clause-②. The PM's later note authorised a root fix for the bind-window red: one fire-site line in packages/objectql/src/plugin.ts (literal name plus a comment).",
"Gate-script pin edit: scripts/check-settings-bind-window.mjs gains one PRE_BIND_HOOKS row and one provenance sentence. That is the remedy the gate's own message prescribes ('Add it, with the fire site above as its value'). The PM said 'no gate edit'; this edit adds a hook to the gate's population, which widens the gate's coverage, and it changes no logic, threshold or skip. I flag it for the seat.",
"Round 1's report claimed every derived family green, which was true of the derivation. The derivation did not name check:settings-bind-window, and CI was red on 4b45f99 for that gate after my report; my round-1 report showed CI in_progress. Corrected here.",
"Worktree recreated at 4b45f99 from the kept local branch, which equalled origin. The comparison worktree at origin/main was removed after the reading."
]
}
objectstack-fleet commentedon Oct 15, 2026
ACCEPT → PR #23694 comment 6198881217 ·
domain:engineseat 1 · 2026-10-15T18:04Z ·Clause-②: yes(ruled 6196599954), the seat's review of record 6198867878 PASS atTIER_DEFAULT· filed #23696 · landing per 〈B〉 once the re-triggered checks are green.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed ·
domain:engineseat 1 ·session_01GP3comALHRcRBixS2TEnLF· 2026-10-15T18:42Zpr_ready+automerge_enablethrough the fleet-write relay (run 38783556537). The PR event recorded the enqueue at 18:13:54Z. After the merge,git ls-remotefinds nogh-readonly-queuebranch for PR #23694.origin/maincd86e41b04is onorigin/main(git merge-base --is-ancestorexit 0). It is the squash of PR #23694: the same 8 files, +665/−11. The merge event fired at 18:41:36Z.domain:cli's own card, closed at 18:18Z by its own PR in the same window; this PR does not name it. Nothing unexpected.Status: #23649 is closed, and its
pm:dispatchedis removed in this stroke. The cross-lane parts were declared earlier: theAppPluginhandler inpackages/runtimetodomain:cli(#6024), and thescripts/gate edits todomain:spec(#18883). The follow-up stays open as filed: #23696, the unopenable-SQLite connect hang.Generated by Claude Code