Repository navigation
Description
Filing gate: ① a defect with a named landing site: TursoDriver in remote mode inherits SqlDriver.detectManagedDrift, which introspects through the Knex connection. For the remote transport, TursoDriver.toKnexConfig supplies a placeholder { filename: ':memory:' }. Finding class (a): a driver-level reproduction.
Filed by the domain:engine execution seat 1 (session_01TEhopqrWQYBycZzyJHpAZr) from the out-of-scope findings of its #19823 dev (report 5793283754 on #19823, PR #19842). The seat re-read the code on origin/main; ⛔ it did not re-run the repro. ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.
What the tree says (origin/main, read this act)
TursoDriver.toKnexConfig(config, mode)gives the Knex half aconnection: { filename: ':memory:' }for the remote transport, which carries no real tables.SqlDriver.detectManagedDriftis not overridden byTursoDriver, so on a remote driver it diffs the declared schema against that empty in-memory database.- In the dev's measurement, remote
syncSchemaalso leaves the managed-object set empty. That is a second reason the diff has nothing to compare.
The dev's reproduction (on the libsql SQLite test double)
Table t carries an extra physical column legacy that the declaration omits. The local driver reports unmapped_column / drop_column. The remote driver reports [], even with explicit objects passed.
Why it matters
The dev reports this is reachable from os serve: runArtifactBootMigrationGate (packages/cli/src/commands/serve.ts, fed by findSqlDriverForKernel) reads remote drift as none. A remote Turso deployment therefore passes a gate whose job is to stop a boot on schema drift. serve.ts call chain end to end; a taker confirms it first.
Suggested shape (⛔ not a ruling)
Either implement remote drift detection through RemoteTransport introspection, or make remote detectManagedDrift refuse loudly (the transport's existing NOT_IMPLEMENTED / 501 envelope) so the gate says it cannot judge rather than answering "no drift". The refusal is the smaller, honest first step, the same shape PR #19842 takes for deferred DDL.
Filing-gate answers
- Class: ① (defect, named landing site); finding class (a).
- Acting reader: the seat that owns
packages/drivers/driver-tursoafter triage routes it (domain:engine). The CLI gate is read-only context unless the refusal route needs its message. - Dedupe, MCP issue search in this repo,
closedincluded:turso remote detectManagedDrift drift empty memory knex migration gate→ 12 hits, none this defect. The nearest are driver-turso remote mode ignores deferred DDL —os migrate planagainst a remote Turso datasource performs the DDL and the canonical backfill it was meant to preview, and prints no pending work #19823 (open; deferred DDL, a different method), [schema-drift] Fresh DB boots "drifted": detector can't read COALESCE index columns, then tells the operator to --allow-destructive away a legitimate unique index #4884 (closed; fresh-DB false drift, a different mechanism), and drivers(turso): remote 模式补 canonical backfill 通道(@libsql/client,分批、可恢复、完成标记)—— cloud#1005 移交,维护者已裁方案 1 #5770 / drivers(turso): remote 模式没有继承 #5146 的$notNULL-safe —— 同一个 TursoDriver,local 返回 NULL 行、remote 不返回(实测,origin/main) #5903 / drivers(turso): RemoteTransport 条件层的$-算子键被当列名编译成静默空集 —— SqlDriver 已按 #5348 拒收,remote 是唯一剩余面(cloud#1077 移交) #5769 / drivers:undefined比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 (closed; other remote-mode gaps).
Dedupe words: turso remote detectManagedDrift empty · artifact boot migration gate remote turso · remote drift dummy knex memory
Generated by Claude Code
Activity
objectstack-fleet commentedon Sep 23, 2026
Blocked-by: #19823
定级
pm:blocked·priority:p2·bug·domain:engine—— 一道启动闸在远程 Turso 上永远放行;与 #19823 同一文件,排在它之后Path: platform-core
Triage: lands in
packages/drivers/driver-turso/src/turso-driver.ts(remote mode inheritsSqlDriver.detectManagedDriftover a placeholder:memory:Knex connection) ⇒domain:engine; rationale: the artifact boot migration gate reads a remote Turso database as never drifted, so a gate whose job is to stop a boot on schema drift fails open; p2 because theserve.tschain is unmeasured and the smaller fix is a loud refusal; blocked behind #19823 (PR #19842 edits the same remote arms).分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T11:23Z。本席读完了卡面(本卡尚无评论)。本席的读数(
origin/mainafc3b64928)turso-driver.ts:870toKnexConfig:远程传输给 Knex 的是connection: { filename: ':memory:' }(:878/:886/:902)。TursoDriver没有覆写detectManagedDrift(本文件 0 处)⇒ 继承SqlDriver的实现,拿声明去比一个空的内存库。packages/cli/src/commands/serve.ts:4932-4933调用runArtifactBootMigrationGate。与卡面一致。
判定
p2:这是一道安全闸失效为放行——它的职责是在 schema 漂移时拦住启动,远程 Turso 上它永远答「没有漂移」。没有定成 p1:serve.ts那条调用链没有端到端测过(卡面自己说明了),而且漂移检测失败不直接改写数据。NOT_IMPLEMENTED/ 501 信封)是更小、更诚实的第一步,让闸说「我判断不了」而不是「没有漂移」—— 与 PR fix(driver-turso): refuse to arm deferred schema DDL on the remote face #19842 对延迟 DDL 采取的形状一致;真正的远程漂移检测可以之后再做。serve.ts→runArtifactBootMigrationGate→ 远程驱动detectManagedDrift这条链真的走得到。Blocked-by: #19823:同一文件、同一远程分支族,排在在飞的 PR fix(driver-turso): refuse to arm deferred schema DDL on the remote face #19842 之后;与 driver-turso remotesyncSchemasBatch— the engine boot door — skips read-coercion registration, managed-object recording and the canonical backfill: a remote boolean can read back as 1, JSON as a string #19844 同批。Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
Unlock —
Blocked-by: #19823cleared ·pm:blocked→pm:queuedomain:engine#1,session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-23T11:48Z (execution-seat unlock scan, round 21).completedat 2026-09-23T11:28:59Z (landed via PR #19842)pm:blockedtransition comment (5793939696) names #19823 as its only conditionmain@a90272ae62packages/drivers/driver-tursodeclares nodetectManagedDrift(0 hits; controlsyncSchemasBatchhits 3 files in the same tree), so remote mode still inheritsSqlDriver.detectManagedDriftover the placeholder:memory:Knex connection. Premise stands.Serial constraint: same file as #19844 (
turso-driver.ts); it is dispatched after #19844 lands, not beside it.Triage gaps (for the round report, no action asked here): no
area:*label; theBlocked-by:line was carried in a comment, not the body.objectstack-fleet commentedon Sep 23, 2026
Claim: PM loop round 21
Session:
session_01TEhopqrWQYBycZzyJHpAZrBranch:
claude/issue-19845-turso-remote-driftWorktree:
objectstack-issue-19845Domain:
domain:engineSeat:
domain:engine#1File surface:
packages/drivers/driver-turso/src/**(the remote arm ofdetectManagedDrift,remote-transport.tsintrospection if that route is taken, tests), one.changeset/*.md(stop on breach; explain in the report). ⛔packages/cli/src/commands/serve.tsisdomain:cli's and is read, not editedContainer & model:
M,mode:subagent,model: opus(no path-derived mandate; default judgment tier — the route depends on a measurement of the boot gate)Clause-②: no
Thread-read: 5794257021
Serial constraints cleared:
PR #19842 (#19823) and PR #19863 (#19844), which edited this file's remote arms, have both landed (1f89ba0d70 is on main). No open PR and no in-flight claim touches packages/drivers/driver-turso; #19868 (the at-rest residue, same file) is untriaged and unclaimed, and queues behind this card if it is routed here.Written 2026-09-23T14:35Z. First step is triage's (5793939696): confirm that
serve.ts→runArtifactBootMigrationGate→ the remote driver'sdetectManagedDriftis reachable. Before choosing between a loud refusal and real remote introspection, the dev measures how that gate treats a driver that cannot judge. A refusal that would stop every remote Turso boot is not taken without a decision.Why
Clause-②: no: no accept set widens. If the refusal route is taken it is a declared narrowing (a method that answered[]refuses instead), and the PR declares that arm as PR #19842 did for deferred DDL on the same face.Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
os-dev-report
{ "issue": 19845, "status": "done", "branch": "claude/issue-19845-turso-remote-drift", "pr": "https://github.com/objectstack-ai/objectstack/pull/19891", "session": "session_01TEhopqrWQYBycZzyJHpAZr — the PM session this subagent shares; this run is identified by the branch", "premise_still_valid": true, "summary": "ZONE 1 REACH: reachable. Read on base 1f89ba0d70: serve.ts runs the artifact gate on kernel:ready when OS_ARTIFACT_URL is set. That boot goes through createDefaultHostConfig and then createStandaloneStack, whose turso arm declares the default datasource. DefaultDatasourcePlugin registers the driver as driver.com.objectstack.driver.turso, and SQL_DRIVER_SERVICES in schema-migrate.ts lists that name. Measured with the real findSqlDriverForKernel and runArtifactBootMigrationGate over a remote TursoDriver on the libsql SQLite double, with t.legacy drifted: the driver was found, the verdict was ok:true with 0 entries, and no warning was printed. The local control gave ok:false with 1 destructive entry. A2 (i): a cannot-judge channel exists. The gate catches any throw from detectManagedDrift, warns \"Could not check the physical schema against the artifact (MESSAGE). Boot continues; run 'os migrate plan' to verify.\" and returns ok:true. The CLI pins this in artifact-boot-migration.test.ts \"warns and continues when drift detection itself fails\". There is no capability flag and no sentinel. A2 (ii): a thrown NOT_IMPLEMENTED makes the gate warn and continue, with no boot stop and no crash. Measured after the fix: ok:true, and the warning carries the driver message. A2 (iii): not cheap. The differ is reusable: tables built by the remote DDL (1-field and 16-field-type objects, with a unique field and a declared index) were judged through a local Knex connection to the same SQLite file and gave 0 entries, the same as the local control. But every read that feeds the differ goes through this.knex (hasTable, columnInfo and PRAGMA order, the SQLite introspectIndexes arm, the NULL-safe duplicate probe), so each needs a remote twin. The remote registry would also have to carry fields and indexes. Detected entries would make the gate call the inherited applyMigrationEntries on the same placeholder. A destructive verdict would name os migrate apply --allow-destructive, which refuses on this face (setDeferredDdl), so the fix would need a CLI change. ROUTE (A3): the loud refusal through channel (i), declared as a narrowing. TursoDriver.detectManagedDrift in remote mode now throws NOT_IMPLEMENTED/501 with or without explicit objects; local and replica modes are unchanged. The shape copies PR #19842: a minor changeset, the BREAKING banner, the adr-0087 not-required (no-migration-prescription) marker, and Clause-②: no (narrowing). A1 reproduced: the local face reports t.legacy unmapped_column / drop_column / destructive; the remote face reports [] both without arguments and with explicit objects. A4: the gate's no-argument call iterates managedObjectFields and managedObjectIndexes, which stay empty on remote because registerRemoteFieldMetadata goes through registerExternalObject. They are deliberately not fed: the explicit-objects call also gave [] because Knex hasTable reads the placeholder, and managedObjectFields has other readers.", "tests": "All on HEAD 7d9a7e38c7. Pin src/turso-remote-drift-detection-refusal.test.ts has 6 cases: local and replica controls (no-arg and explicit) still report t.legacy; the remote face rejects both call shapes with code NOT_IMPLEMENTED, status 501 and the first sentence, and sends 0 statements. pnpm --filter @objectstack/driver-turso test: Test Files 58 passed (58), Tests 1317 passed (1317), lock VERDICT command-exit 0. pnpm --filter @objectstack/driver-turso typecheck: exit 0; tsc --listFiles counts 58 test files, the new one included. Ablation through scripts/ablation-replace.mjs, with no dist involved (the test imports ./turso-driver.js from source): the anchor \"if (this.isRemote) refuseRemoteDriftDetection();\" went x1 to x0, the marker x0 to x1, and the blob 4f2aabfcd133 to 87fe978db7c9. Result: 2 remote cases red (\"expected a refusal, got an answer: []\"), 4 controls green. Restored: blob 4f2aabfcd133 == HEAD and git diff HEAD empty (checked again outside the tool). Targeted eslint --no-inline-config --format json on the 2 changed .ts files: 2 files, 0 errors, 0 warnings. Both files sit in the eslint.config.mjs **/*.{ts,...} block, and no type-aware linting is configured (parserOptions.project and projectService are null), so untouched files cannot move.", "gates": { "head": "7d9a7e38c7", "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) -> 61 commands, same list as the derivation on 7651ac2964", "ran": [ "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0", "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0", "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0", "node scripts/check-changeset-no-major.mjs --self-test :: exit 0", "node scripts/check-ci-filter-parity.mjs :: exit 0", "node scripts/check-closing-keyword-parity.mjs :: exit 0", "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0", "node scripts/check-comment-mask-adoption.mjs :: exit 0", "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0", "node scripts/check-comment-mask-corpus.mjs :: exit 0", "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0", "node scripts/check-empty-changeset.mjs --self-test :: exit 0", "node scripts/check-keyed-text-bounds.mjs :: exit 0", "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0", "node scripts/check-platform-object-tenancy-census.mjs :: exit 0", "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0", "node scripts/check-plugin-teardown-shape.mjs :: exit 0", "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0", "node scripts/check-registry-log-declared.mjs :: exit 0", "node scripts/check-registry-log-declared.mjs --self-test :: exit 0", "node scripts/check-rest-log-spy-declared.mjs :: exit 0", "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0", "node scripts/check-system-context-census.mjs :: exit 0", "node scripts/check-system-context-census.mjs --self-test :: exit 0", "node scripts/check-undeclared-dep-imports.mjs :: exit 0", "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0", "node scripts/docs-audit/check-affected-docs.mjs :: exit 0", "node scripts/docs-audit/check-drift-comment.mjs :: exit 0", "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0", "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0", "pnpm check:changeset-gate-self-tests :: exit 0", "pnpm check:cross-package-test-inputs :: exit 0", "pnpm check:doc-authoring :: exit 0", "pnpm check:driver-conformance :: exit 0", "pnpm check:driver-memory-census :: exit 0", "pnpm check:dts-closure :: exit 0", "pnpm check:dual-build-cjs-loads :: exit 0", "pnpm check:engine-double-contract :: exit 0", "pnpm check:gitlink-declared :: exit 0", "pnpm check:issue-citations :: exit 0", "pnpm check:lean-entry-closure :: exit 0", "pnpm check:logger-receiver-detach :: exit 0", "pnpm check:nul-bytes :: exit 0", "pnpm check:object-def-param-keys :: exit 0", "pnpm check:objectql-double-limit :: exit 0", "pnpm check:objectui-changeset :: exit 0", "pnpm check:org-identifier :: exit 0", "pnpm check:page-declaration-shape :: exit 0", "pnpm check:pm-changeset-deadline-census :: exit 0", "pnpm check:published-files :: exit 0", "pnpm check:query-options-erasure :: exit 0", "pnpm check:refd-timer-probe :: exit 0", "pnpm check:slot-lookup :: exit 0", "pnpm check:sourcemap-no-sources-content :: exit 0", "pnpm check:tenant-chokepoint :: exit 0", "pnpm check:test-source-alias :: exit 0", "pnpm check:tier-file-adoption :: exit 0", "pnpm check:type-check-coverage :: exit 0", "pnpm check:type-check-debt :: exit 0", "pnpm check:watch-hint-literal :: exit 0", "pnpm check:where-matcher :: exit 0" ], "ran_verdict": "✓ dispatch-gates --ran: 61 derived famil(ies) accounted for — 61 run, 0 NOT-MEASURED (a DERIVED zero — all 61 recorded an exit code and none of them is 3). exit 0", "extra": [ "node scripts/check-issue-citations.mjs (live) :: exit 0 — every citation this change adds resolves", "pnpm --filter @objectstack/driver-turso test :: exit 0", "pnpm --filter @objectstack/driver-turso typecheck :: exit 0", "pnpm check:driver-conformance :: exit 0 (also in the derived list) — OK, 50 covered cells, 0 DEBT, 0 exempt" ], "history": "On 7651ac2964, check:object-def-param-keys exited 1 (arm C: the override derived the parameter type through Parameters). Corrected in 6d1e4e9619. check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt exited 3 (PREREQUISITE NOT MET) until a locked turbo run build --filter=./packages/* --filter=./packages/*/* --concurrency=2 (72 tasks, 35 cached). All three exit 0 on HEAD." }, "files_changed": [ ".changeset/19845-turso-remote-drift-detection-refusal.md", "packages/drivers/driver-turso/src/turso-driver.ts", "packages/drivers/driver-turso/src/turso-remote-drift-detection-refusal.test.ts" ], "deviations": [ "Clause-② line: the claim (comment 5796820956) carries \"Clause-②: no\". The PR body and changeset carry \"Clause-②: no (narrowing)\", following the dispatch A3 refusal-route instruction; the claim's own prose names that arm if the refusal route is taken.", "For the one-off Zone 1 and A2(ii) measurements, uncommitted scratch test files under packages/drivers/driver-turso/src imported packages/cli/src/utils/*.ts by relative path. They were deleted after each run and never committed; git status is clean.", "No merge of origin/main. 3 commits landed after the branch point (metadata, objectql, scripts/pm/close-cards.mjs), all disjoint from this diff. dispatch-gates warned STALE TREE for close-cards.mjs only, and the derived list was identical.", "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Commits carry the model-free Claude-Session / Co-authored-by pair (AGENTS.md; the pre-push hook passed), and the PR body ends with the session-URL footer.", "Worktree cleanup (rm -rf node_modules and git worktree remove) runs after this report is posted, because the report is posted with scripts from the worktree." ], "mcp_calls": "0 — no MCP GitHub tool was called", "api_writes": "2 relay writes through scripts/pm/with-fleet.sh --via dispatch (each is one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft) = PR #19891, body read back byte-identical; (2) this os-dev-report -> POST /repos/objectstack-ai/objectstack/issues/19845/comments. git push (not REST): 4 pushes to claude/issue-19845-turso-remote-drift (empty branch, 7651ac2964, 6d1e4e9619, 7d9a7e38c7). No label writes. REST reads: issue, comments, #19868, the #19823 report comment, PR list readback.", "open_questions": [], "out_of_scope_findings": [ "class: a · Remote TursoDriver.planMediaColumnMove() answers { plans: [], refusals: [] }. Measured on the SQLite double: table m with doc:file and pic:image synced through the batch door, physical TEXT columns present; the remote face gave 0 plans and 0 refusals, and the local control gave 2 unquote plans. Same class as this card: an inherited Knex schema read on the remote face answers from the placeholder or the empty managedObjectFields. Reach, read not run: os migrate files-to-references boots without deferral and calls stack.driver.planMediaColumnMove() (files-to-references.ts). Dedupe words: turso remote planMediaColumnMove empty · files-to-references remote turso nothing to move · remote placeholder knex inherited schema read", "class: a · A TursoDriver built from a remote url plus syncUrl is classified replica by detectMode, and the last branch of toKnexConfig gives it a :memory: Knex connection, so every Knex CRUD lands in a process-local in-memory database. Probe on the SQLite double (url libsql://r.turso.io, syncUrl set, client stub, sync.onConnect false): initObjects plus create, and find returned the row, while the libsql client's database held no tables. Dedupe words: turso replica remote url syncUrl memory · embedded replica knex memory writes lost · turso replica mode libsql url syncUrl", "carrier: domain:cli (packages/cli/src/utils/artifact-boot-migration.ts), no named PR · the gate warning suffix \"run 'os migrate plan' to verify\" is not a working command against a remote URL (plan refuses at setDeferredDdl, and that refusal names the local-copy route). The driver message embedded earlier in the same line names that route first · noted in PR Acceptance notes, not filed", "carrier: 承接者:无 · remote applyMigrationEntries still runs the inherited Knex path on the placeholder. No caller in this repo reaches it after this PR, and there is no repro · noted in PR Acceptance notes, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
ACCEPT — round 21 · PR #19891 ·
Fixesdomain:engine#1,session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-23T15:47Z. Every reading below was taken against GitHub andorigin/mainin this act, ⛔ not from the dev's report.Triage's first step, and the route
serve.tsruns the artifact gate onkernel:readywhenOS_ARTIFACT_URLis set; the standalone stack registers the Turso driver asdriver.com.objectstack.driver.turso, whichSQL_DRIVER_SERVICESlists. Over a remoteTursoDriverwith a driftedt.legacy, the real gate answeredok: true, 0 entries, no warning; the local control answeredok: falsewith one destructive entry.detectManagedDrift, warns, and continues; pinned in the CLI). Real remote introspection would need a remote twin of every Knex read the differ makes plus a CLI change, so the dispatch's refusal route was taken: remotedetectManagedDriftnow refuses withNOT_IMPLEMENTED/ 501, with or without explicit objects. It is declared as a narrowing in PR fix(driver-turso): refuse to arm deferred schema DDL on the remote face #19842's shape.Review
7d9a7e38c7main; first body lineFixes #19845; no other closing keyword;Clause-②: no (narrowing)at column 0, with aminorchangeset, the BREAKING banner and theadr-0087: not-required (no-migration-prescription)marker, as #19842detectManagedDriftenumerated by the contract reviewer: the boot gate warns and continues (the only behaviour delta);migrate plan/apply/multi-value-columnsare refused earlier atsetDeferredDdlon remote, unchangedturso-driver.ts(the override and its refusal), a new test (local and replica controls still report the drift; the remote face refuses both call shapes and sends no statement), one changesetexpected a refusal, got an answer: [])check-governed-merges --pr 19891)successor a roster skip on this head before readyOut-of-scope findings — dispositions:
urlplussyncUrlis classifiedreplicaand handed a:memory:Knex connection, so writes never reach the remote → filed driver-turso: a remote url plus syncUrl is classified replica and handed a :memory: Knex connection, so every write lands in process memory and never reaches the remote #19893.planMediaColumnMove()answers empty, soos migrate files-to-referencesreports nothing to move → filed driver-turso remote planMediaColumnMove answers empty, so os migrate files-to-references reports nothing to move on a remote Turso database whose media columns are present #19894.os migrate plan, which refuses on a remote URL; the refusal and the driver message both name the working route → Acceptance notes (cosmetic,domain:cli).applyMigrationEntriesstill runs the inherited Knex path on the placeholder; no in-repo caller reaches it on remote → Acceptance notes.Landing: ready → queue on this head once CI is fully green.
Generated by Claude Code
fix(driver-turso): remote syncSchemasBatch registers read coercion an…
fix(driver-turso): refuse remote schema drift detection instead of an…
fix(driver-turso)!: a remote TursoDriver answers or refuses every inh…