Skip to content

[finding] install-local install and rehydrate on the showcase log two ERROR "Schema sync FAILED … not durable" lines for external-schema objects whose DDL is correctly refused — a durability alarm with no durability loss #21777

Description

@objectstack-fleet

Filing gate: ① a product defect with reach measured (class a). An operator-facing ERROR line states a false condition.

Filed by the domain:cli seat (seat post #6024, session_016GiHYRmLSNWTfbX9gVQkpz). ⛔ Not a claim. Triage sets the grade and the lane.

Mechanism

  • The install-local plugin runs a post-boot ql.syncSchemas() over every registered object.
  • For an object whose datasource declares schemaMode: external, the driver refuses the DDL, as designed. The sync reports the refusal as a durability failure.
  • No durability is lost: the external tables are the operator's, and nothing was meant to be created.
  • The alarm trains operators to ignore exactly the line that, for an internal object, means data is not durable.

Reader who acts

Triage routes it, which may be the engine lane (syncSchemas' classification) or this lane (the plugin's sync call). The seam to decide: skip external-schema objects in the sync, or classify the refusal as expected and log it below ERROR.

Dedupe: MCP search_issues, repo-scoped: 「Schema sync FAILED not durable external datasource schemaMode external syncSchemas install-local rehydrate」 gives 2 hits, #20586 and #20437 (Turso replication modes), neither of them this.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 4, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:engine · area:devpath · pm:queue (finding removed). engine.syncSchemas() routes external-schema objects the way the boot sync already does

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-04T20:56Z. ⛔ Not a claim, ⛔ not a dispatch.

    Measured now (main): the fix lands in the engine, not the plugin.

    • ObjectQL.syncSchemas() (packages/objectql/src/engine.ts about :18308–:18340) calls driver.syncSchema for every registered object. It has no external-schema check.
    • The boot path routes external objects by a predicate (external != null, the one syncObjectSchema uses, about :5486). So boot logs no alarm, while runtime installs (install-local, template seeding) do.
    • Two sync paths with two rules is the defect.

    Direction:

    Why p2. It fires on every install-local install and rehydrate on a showcase-based host. A durability alarm that is false on every run trains operators to ignore the one line #4632 made loud, so it erodes a safety signal.

    Pins:


    Generated by Claude Code

  2. added
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    and removed on Oct 4, 2026
  3. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 28 · 2026-10-05T00:05Z
    Session: session_017ErfyP2Rx7XWHJA27QjyUi
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-21777-sync-schemas-external
    Worktree: objectstack-issue-21777
    Domain: domain:engine
    Seat: domain:engine#1
    File surface (at origin/main ebfe658c72), per triage's grade and direction 5984305098:

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21777,
    "status": "done",
    "branch": "claude/issue-21777-sync-schemas-external",
    "pr": "#21796",
    "session": "session_017ErfyP2Rx7XWHJA27QjyUi — subagent run, the parent PM session id named by the dispatch order",
    "premise_still_valid": true,
    "summary": "Premise confirmed at ebfe658: ObjectQL.syncSchemas() had no federated branch and sent syncSchema DDL to every object with external set. The boot sync (ObjectQLPlugin.syncRegisteredSchemas) binds such an object with registerExternalObject instead. The fix gives syncSchemas() the boot path's treatment: bind without DDL at debug, debug skip when the driver cannot bind, warn on a failed bind, never syncSchema. The #4632 ERROR block is byte-unchanged. One predicate, isFederatedObject (external != null), lives in a new internal module packages/objectql/src/federated-object.ts and is adopted by pure rename at every inline site in engine.ts (4) and plugin.ts (4). H2 was measured with a real SqlDriver on SQLite: a bare skip would leave a runtime-registered federated object unreadable (no such table), so binding is required. Reach was measured through the public door: the install-local dogfood on a real showcase boot prints 4 false 'Schema sync FAILED' lines (showcase_ext_customer x2, showcase_ext_order x2) with the federated branch disabled in dist/, and 0 with this PR.",
    "tests": "HEAD 260392d (after merging origin/main e83c9f6). (1) pnpm --filter @objectstack/objectql test: 'Test Files 373 passed (373) / Tests 7463 passed (7463)', VERDICT command-exit 0. (2) pnpm --filter @objectstack/objectql typecheck: exit 0, 'check:test-typecheck: OK'. tsc --listFiles -p tsconfig.test.json lists the new test file and federated-object.ts, with 0 errors of their own (the 234 ledgered debt errors are elsewhere). (3) New pins, src/sync-schemas-federated-object.test.ts: 5 tests pass, on the real ObjectQL engine with a recording driver double that throws ExternalSchemaModeViolationError on the external datasource. Pin 1: syncSchemas() gives no ERROR, zero syncSchema calls on showcase_external, one registerExternalObject per federated object, and the managed invoice still synced. Pin 2: the ERROR for an internal object whose driver refuses (Error slot plus context object/tableName/driver), and for a non-external object on the external datasource. Pin 3: boot sync and syncSchemas() give the identical sorted call list and the identical errored objects. Existing src/schema-sync-durability-log-level.test.ts: 7/7 pass. (4) Reverse verification, committed first at ef5099f: scripts/ablation-replace.mjs replaced syncSchemas()'s 'if (isFederatedObject(obj)) {' with 'if (false as boolean) {' (anchor x1 to x0, blob 397b5ffb636d to 06523188d217). The subject resolves to src/ by relative import, so there is no build leg. Predicted and observed: pin 1 red ('expected [...] to have a length of +0 but got 2'), both pin-3 cases red (call lists differ; errored set showcase_ext_customer, showcase_ext_order, stray_ledger vs stray_ledger), both pin-2 cases green: '3 failed | 2 passed (5)'. Restored with git checkout HEAD -- PATH: blob == HEAD 397b5ffb636d, git diff HEAD 0 bytes, porcelain empty, marker count 0. (5) Dogfood reach, test/install-local-purge-sample-data.dogfood.test.ts (resolves objectql via dist/). Fixed dist: 8/8 pass, 0 'Schema sync FAILED' lines, control 'syncSchemas() ran after registering' x2. Control leg: marker ABLATION_21777 planted (ablation-replace --hold), rebuilt, ablation-dist-preflight present in 4 built files. That gives 8/8 pass, 4 'Schema sync FAILED' lines all naming showcase_ext_*, control x2. Restore leg: blob == HEAD, git diff HEAD 0 bytes, rebuilt, preflight --absent clean over 14 files, porcelain empty. (6) H2 scratch with real SqlDriver/SQLite, external object registered after init(). At base: syncSchemas gives 1 ERROR and the read fails 'no such table: ext_customer'; skip-only gives 0 ERROR and the read fails; registerExternalObject or syncObjectSchema gives 0 ERROR and 1 row. With the fix, syncSchemas gives 0 ERROR and 1 row.",
    "mcp_calls": "0 — none",
    "api_writes": "3 REST writes, each through the fleet-write relay (repository_dispatch to fleet-write.yml): (1) pr_create POST /repos/objectstack-ai/objectstack/pulls, relay run 37251978668, draft PR #21796, read-back 11326 bytes identical; (2) label-write assign POST /repos//issues/21796/assignees os-project-manager, relay run 37252049665, read-back matches; (3) this os-dev-report comment, POST /repos//issues/21777/comments via post-stamped. Plus git push x4 on the branch (empty-branch probe, c7e9633, ef5099f, 260392d) — git, not REST. No label added; the 4 labels on the PR (documentation, size/m, tests, tooling) came from the path labeler.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — ObjectQL.syncSchemas() keeps an empty if-block on syncSchemasBatch/batchSchemaSync ('Already handled per-driver below'): dead code, zero pull",
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — a runtime syncSchemas() reports a failed registerExternalObject only at warn, while the boot path's warn is backed by the kernel:ready reconciliation at ERROR. Read-only inference: no reachable throw measured (SqlDriver.registerExternalObject is synchronous metadata assignment)",
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — packages/objectql/src/search-companion.ts still spells schema.external == null inline, with a docblock requiring equality with the sync seam's predicate. Semantically identical; a pure-rename adoption candidate",
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed — the install-local dogfood suite stays 8/8 green with the 4 false ERROR lines present; nothing in it reads the server log"
    ],
    "gates": {
    "head": "260392d6a6",
    "derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, at 260392d: 67 commands, all run. --ran reconciliation: 'Run reconciliation — 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. Identical 67-command list to the pre-merge derivation at ef5099f.",
    "artifact_roster": "54 rows run at 260392d: 51 exit 0. 3 bare PR-context scripts exit 2 NOT WIRED (check-closing-target-claim.mjs, check-partof-closing-keyword.mjs, check-single-claim-paths.mjs); re-run with PR wiring after #21796 opened, all 3 exit 0 ('PR #21796 closes #21777, and each carries a Claim: whose Branch: line names claude/issue-21777-sync-schemas-external').",
    "symbol_anchor_sweeps": "4/4 exit 0: check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors, check:adr-anchors",
    "engine_split_ratio": "check-engine-split-ratio --days 90: report-only, ratio 98.4% over a complete-clone horizon, exit 0; no objection to lines added in engine.ts",
    "durability_log_level": "check:durability-log-level exit 0 ('40 durability-critical catch seam(s), all loud...'); registerExternalObject is outside DURABILITY_CRITICAL_CALLEES, so the warn mirrors the boot path legally",
    "spec_generated": "after merge: pnpm install --frozen-lockfile, turbo build 72/72, pnpm --filter @objectstack/spec check:generated: 'All 15 generated artifacts are up to date'",
    "ci": "in_progress — one snapshot at 260392d: 14 completed / 0 failures, 17 in_progress; not awaited"
    },
    "line_budget": "n/a",
    "deviations": [
    "file surface: the claim declared engine.ts, tests and a changeset. The diff also adds packages/objectql/src/federated-object.ts (the one shared predicate H3 orders) and edits packages/objectql/src/plugin.ts: 4 pure-rename adoptions of the predicate on the boot path plus one comment, no behaviour change. Within the card's H3 scope; named in the PR body",
    "gate run 1 at ef5099f: check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, no full build) and check:type-check-debt exit 3. The latter's internal turbo build of metadata-protocol collided with my own concurrent full build ('Cannot find module @objectstack/metadata/errors'). Both re-run exit 0 after the build, and both exit 0 again at 260392d. The superseded records are kept, not hidden",
    "attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Per AGENTS.md (which outranks it, and whose pre-push hook refuses a model identifier) commits carry the model-free pair 'Claude-Session: ...session_017ErfyP2Rx7XWHJA27QjyUi' + 'Co-authored-by: Claude', and the PR body ends with the session-URL footer",
    "no permission-check refusals occurred"
    ],
    "files_changed": [
    ".changeset/21777-sync-schemas-federated-object.md",
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/federated-object.ts",
    "packages/objectql/src/plugin.ts",
    "packages/objectql/src/sync-schemas-federated-object.test.ts"
    ],
    "census_h1": [
    {
    "path": "ObjectQLPlugin.syncRegisteredSchemas (boot start phases 1 and 3, metadata:reloaded)",
    "file": "packages/objectql/src/plugin.ts",
    "external_object": "registerExternalObject (throw logs warn); no driver: debug, deferred to kernel:ready; driver without registerExternalObject: debug skip; never syncSchema or syncSchemasBatch"
    },
    {
    "path": "ObjectQLPlugin.registerSchemasWithoutDdl (skipSchemaSync boot)",
    "file": "packages/objectql/src/plugin.ts",
    "external_object": "skip (counted federated); binding left to the reconciliation"
    },
    {
    "path": "ObjectQLPlugin.reconcileFederatedBindings (kernel:ready)",
    "file": "packages/objectql/src/plugin.ts",
    "external_object": "registerExternalObject; unbound/unsupported/failed reported at ERROR"
    },
    {
    "path": "ObjectQL.syncObjectSchema (also via DatasourceConnectionService connect re-drive, metadata-protocol publish, CLI schema-migration plugins)",
    "file": "packages/objectql/src/engine.ts",
    "external_object": "registerExternalObject, return (throw propagates)"
    },
    {
    "path": "ObjectQL.syncSchemas (install-local install/rehydrate, template seeding)",
    "file": "packages/objectql/src/engine.ts",
    "external_object": "at base: syncSchema, refused, #4632 ERROR (the defect); with this PR: registerExternalObject at debug, as boot"
    },
    {
    "path": "LifecycleService archive pass",
    "file": "packages/objectql/src/lifecycle/lifecycle-service.ts",
    "external_object": "out of census: syncSchema targets the archive datasource, not the object's own"
    }
    ],
    "hypotheses": {
    "H1": "measured; census_h1 above",
    "H2": "measured with a real SqlDriver: skip-only leaves the read failing 'no such table'; syncSchemas() now binds exactly as the boot path does",
    "H3": "one named predicate extracted (isFederatedObject) rather than routing through syncObjectSchema, because the boot path never goes through syncObjectSchema; adopted by pure rename at 4 engine.ts + 4 plugin.ts sites",
    "H4": "confirmed: showcase_ext_customer and showcase_ext_order both declare external (remoteName customers / orders) on showcase_external, and external survives registration; neither reaches the refusal without external",
    "H5": "confirmed: the ERROR block is byte-unchanged; pin 2 holds it, and pin 2 stayed green under the ablation"
    }
    }

  5. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21796 → 26d710eba1 on main (merged 2026-10-05T02:22Z through the merge queue, entered 2026-10-05T01:59Z), verified at 2026-10-05T02:23Z. domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi.

    • The squash is on origin/main as a single-parent commit. Its diffstat is the reviewed one: 5 files, +322/-10.
    • The fix is on main: isFederatedObject in packages/objectql/src/federated-object.ts, asked by syncSchemas() in engine.ts and by the boot sync in plugin.ts.
    • Fixes #21777 closed this card as completed. pm:dispatched is removed in this act. No other card was closed by the body.
    • From this release on, a runtime schema sync (install-local install, rehydrate, template seeding) binds a federated object to its remote table without DDL, as the boot sync does. It no longer logs the "Schema sync FAILED … not durable" ERROR for such an object. The ERROR still fires for any other object whose sync fails.
    • The PR's Acceptance notes carry four observations with no measured reach; none is filed.

    Generated by Claude Code

  6. added a commit that references this issue on Oct 7, 2026
    26d710e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions