Skip to content

feat(driver-turso)!: timeout bounds remote operations; localPath and wasm leave the published config schema (ADR-0049 enforce-or-remove) - #16376

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-16024-turso-inert-config-keys
Draft

feat(driver-turso)!: timeout bounds remote operations; localPath and wasm leave the published config schema (ADR-0049 enforce-or-remove)#16376
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-16024-turso-inert-config-keys

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #16024

Clause-②: yes

Ruling (director seat, decision batch #60, 2026-09-06T15:25:55Z; maintainer reply verbatim 「同意」), per key: TursoDriverConfig.timeout forward, TursoConfigSchema.localPath remove, TursoConfigSchema.wasm remove. Not a rename for any of the three. This PR disposes of all three, so it closes the card.

1. The verification the ruling handed over — what @libsql/client's Config exposes

Read in this worktree from @libsql/core@0.17.4 (lib-esm/api.d.ts; the package resolves @libsql/client@0.17.4, @libsql/hrana-client@0.10.0; manifest range ^0.17.3):

  • Config.timeout?: number exists, documented verbatim as "Busy timeout in milliseconds for local file: databases … It only takes effect for local SQLite databases; remote clients ignore it." That is a lock-contention busy timeout, not the operation timeout the driver's docblock promises. Forwarding to it would have kept remote mode exactly as inert as before and given replica mode a different setting under the same name. Not forwarded.
  • Config.fetch?: Function is the custom fetch for the HTTP client. On the client source: http.js hands it to hrana.openHttp(...), whose HttpClient uses it for every request, the protocol-version probe (findEndpoint) included; ws.js never reads fetch; and sqlite3.js (the file: + syncUrl embedded-replica arm) forwards only authToken / encryptionKey / remoteEncryptionKey / syncUrl / syncPeriod / readYourWrites / offline / timeout to the native libsql Databasefetch never reaches that arm.

So the ruling's fallback ("wrap the client's fetch with AbortSignal.timeout(ms) on the remote and replica arms") lands as written on the remote HTTP arm and cannot land on the replica arm — there is no fetch there. On the replica arm the only remote operation the driver performs is sync(), so the window is applied to the awaited sync() itself (a race against the deadline; the native sync is not cancellable, only no longer awaited). Both arms now fail a stalled remote inside the configured window with the ADR-0112 envelope code: TIMEOUT, status: 504. wss:// / ws:// remote URLs ride the WebSocket transport, which exposes no seam of either kind in 0.17.4; the docblock and README say so rather than promising it.

2. What changed, per key

TursoDriverConfig.timeout (forward). turso-driver.ts: the two remote createClient sites (connect() and the transport's lazy connect factory) now go through one private createRemoteClient(), which passes fetch: fetchBoundedBy(timeout) when the key is set and greater than 0 — the drift between two hand-copied createClient calls is how timeout sat unforwarded four lines from a forwarded concurrency. fetchBoundedBy aborts with AbortSignal.timeout, honours an upstream request signal via AbortSignal.any, and translates only its own abort into the envelope. sync() is bounded by boundedBy(...) with the same envelope. 0 / unset = no bound (what the published schema already documented). The key keeps its bare spelling: the datasource seam in @objectstack/service-datasource already maps the authored config.timeoutMs onto TursoDriverConfig.timeout, citing this card as the reason the driver key was not renamed — so an authored timeoutMs that used to be silently dropped now bounds the connection.

TursoConfigSchema.localPath and .wasm (remove). Same treatment as this package's timeouttimeoutMs rename: the shape is a plain z.object (STRIP posture), so a bare deletion would have stripped both keys in silence; they stay declared as z.never({ error }) tombstones whose .describe() is [REMOVED] ... plus the prescription (tsc refuses them on TursoConfig; the parse raises the prescription naming what actually does the job — url for the replica file, the remote arm for a runtime without native bindings — and the standardized os migrate meta --from 17 sentence). timeoutMs's describe now says what the code does ("Operation timeout in milliseconds for remote operations (0 = no bound)"). No internal issue id in any customer-facing string.

Docs following the code: README :208 docblock; docs/design/driver-turso.md config table (dropped the two rows, timeout row → timeoutMs with the real behaviour, and corrected its claim that the schema lives only in packages/spec — the package mirror is where these keys lived).

ADR-0087: D3 semantic entry driver-turso-config-local-path-wasm-retired (packages/spec/src/migrations/entries/semantic/18.…ts, registry regenerated with gen:migration-registry). Semantic rather than a D2 conversion because @objectstack/spec's own turso contract (data/TursoConfig, strict) never declared either key, so no stack source or stored datasource row that passed the spec door can carry them, and a value that never did anything has no lossless rewrite. Measured: semantic entries project into registry.ts only — sibling id ui-record-blocks-unknown-keys-refused has 0 hits in spec-changes.json and the upgrade guide, same as mine — so check:generated reporting all 15 artifacts current is a real reading, not a missed regeneration.

Changeset: @objectstack/driver-turso minor with the BREAKING banner and a Wrote / Write-instead migration table (the gate refuses major; the removal narrows a published config surface, so nothing lower than minor is honest), @objectstack/spec minor for the ledger entry (a new member of the published migration registry is an additive widening of that package's surface, and the gate's WHICH LEVEL rule sets the floor at minor — it was patch at a0c4b39, corrected at 8c3dd7b), marker adr-0087: registered driver-turso-config-local-path-wasm-retired.

3. Premises re-derived on origin/main (f377394), not on the card's feature branch

  • 2.3 holds. Over packages/drivers/driver-turso/src/ with tests excluded and comment lines stripped: concurrency (positive control) is declared at turso-driver.ts:91, spec/turso.zod.ts:95 and forwarded at :433, :564, :585; timeoutturso-driver.ts:108 declaration only; localPathspec/turso.zod.ts:110 declaration only; wasmspec/turso.zod.ts:153 declaration only (the one other hit, remote-transport.ts:3255, is the package name driver-sqlite-wasm inside a message string).
  • 2.4 holds for localPath and wasm — key-shaped grep on packages/spec/src/data/driver/turso.zod.ts returns 0 for each, the timeoutMs control returns :224. The spec side does declare timeoutMs (+ a retired timeout tombstone) since [#14478 stack 5/6] data/ · ui/ · ai/ · integration/: the 7 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers #15680, which is what makes the forward limb reach a real authored key.
  • 2.5, with a falsification of the triage premise. The package schema is a plain z.object: unknown keys STRIP, so a bare deletion would have been a silent narrowing; the tombstones make it a loud refusal by choice, which is the right shape. Blast radius, measured: the host-side driver registry (packages/spec/src/data/driver/config-registry.zod.ts:348/369) maps turso to the spec-side TursoConfigSchema / getTursoConfigJsonSchema — not the package's. In-repo consumers of the package export: none outside its own tests; objectui (../objectui at the pinned checkout): none. So "the schema a host reads to render Turso configuration UI" is the spec-side one, which never had these keys; the removal reaches only direct npm consumers of TursoConfigSchema from @objectstack/driver-turso.
  • 2.1 (clause ② by name set) partly falsified. The exported-name set of the built declarations is 28 → 28, ADDED ∅, REMOVED ∅ — the two keys are members of one exported schema type, not exports, so a removal does not show as a non-empty REMOVED set here. It shows one level down, see §4.

4. Contract review (clause 2) — measured on the built declarations

pnpm build of the package at base (dist restored from the turbo cache, index.d.ts sha 7152306…) and at head (real rebuild: index.js mtime 1788710110 → 1788711166, index.d.ts sha fccf5bc…, 97.08 KB), index.d.ts and index.d.mts byte-identical to each other on both sides. Diff of the published declaration, every hunk classified:

  • TursoDriverConfig.timeout JSDoc expanded — comment only, not surface.
  • private createRemoteClient; added — a private member's bare-name line, not public surface.
  • sync() JSDoc expanded — comment only.
  • localPath: z.ZodOptional of z.ZodStringz.ZodOptional of z.ZodNeverdeclaration text moved.
  • wasm: z.ZodOptional of z.ZodBooleanz.ZodOptional of z.ZodNeverdeclaration text moved.

The last two change what the published TursoConfigSchema accepts: { url, localPath } and { url, wasm } are newly rejected, at compile time and at parse. That is a request newly rejected on a published contract, and timeout newly changes runtime behaviour on a published constructor option ⇒ clause 2: yes, declared on the card's claim comment and carried by needs:contract-review on both carriers. This round does not clear its own contract review.

5. Tests

  • New src/turso-driver-timeout.test.ts (5 cases): remote HTTP arm against a real http.Server that accepts the connection and never responds — with timeout: 100 the find fails as TIMEOUT / 504 naming the window, the server saw ≥ 1 request; negative control without timeout is still pending after 1000 ms. Replica arm with a stub client whose sync() never settles — timeout: 100 rejects TIMEOUT / 504; controls: no timeout and timeout: 0 both still pending after 1000 ms. Firing control on the unmodified tree: the two positive cases were red (each hung to vitest's 5 s cap), the three controls green.
  • src/spec/turso.test.ts: refusal-envelope pins for localPath and wasm (path, was removed, the url / nothing selects a WASM build clause, Delete the key, os migrate meta --from 17), wasm: false refused too, tombstones absent from a parse that never wrote them; the two fixtures that authored localPath / wasm rewritten to the shape the driver reads (they were the measurement: accepted, asserted, consumed by nothing).
  • Package suite at a0c4b39: vitest run --maxWorkers=245 files, 1178 tests passed. typecheck exit 0, and tsc --noEmit --listFiles lists all four edited/added src/** files in the 51-file program (the package tsconfig includes src/**/*), so the typecheck green covers the test files.

6. Gates run locally — code/test/build union at a0c4b39; the changeset family re-run at the final head 8c3dd7b (git rev-parse --short HEAD = 8c3dd7b; the two trees differ only in .changeset/driver-turso-inert-config-keys.md, the spec bump patchminor)

Derived with node scripts/pm/dispatch-gates.mjs (no path argument). Green: check:nul-bytes; check-adr-0087-registration --base origin/main ("1 declared-breaking changeset(s), each carrying an ADR-0087 disposition") + --self-test; check-changeset-no-major; check-empty-changeset; check:changeset-gate-self-tests; check:doc-authoring; check:cross-package-test-inputs (117/117); check:published-files; check:test-source-alias; check:driver-conformance (driver-turso row all ok); check:query-options-erasure; check:spec-parsed-alias; check:where-matcher; check:objectql-double-limit; check:logger-receiver-detach; check:tenant-chokepoint; check:slot-lookup; check:org-identifier; check:page-declaration-shape; check:merge-driver; check:objectui-changeset; check:dts-closure (8 built packages swept — the driver-turso dependency closure — 46/46 declaration files present); check:type-source-resolution; check:adr-anchors; check:cross-package-test-inputs; system-context-census, comment-mask-adoption, keyed-text-bounds, platform-object-tenancy-census, plugin-teardown-shape, registry-log-declared, undeclared-dep-imports, ci-filter-parity, reference-carrier-shape, dev-prereqs, release-rehearsal-clone (each with --self-test where the workflow runs one); docs-audit check-affected-docs / check-drift-comment. pnpm --filter @objectstack/spec build then check:generated: all 15 artifacts current; the derived spec source audits duration-unit-keys (215 keys, 0 offenders), browser-reachable-entries, entry-nameability, exported-any, dual-source-exports, llms-txt, objectui-pin-citations, variant-docs, empty-state, liveness, strictness-ledger, skill-refs all green. Repo-wide pnpm lint (eslint . --no-inline-config): exit 0 in 117 s — the full scan, not a narrowing.

Not measured (exit 3 = prerequisite not met, CI owns them): check:dual-build-cjs-loads (needs the whole workspace built; connector-mcp dist absent here) and @objectstack/lint check:doc-formula-expressions (needs @objectstack/lint built).

7. Out of scope, left alone

Authored in Claude Code session session_01ARYe3yQTQCUFm5qPYNgKaJ, branch claude/issue-16024-turso-inert-config-keys.


Generated by Claude Code

…sm tombstoned (#16024)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-turso, @objectstack/spec, touching 11 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/drivers/driver-turso/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/data-modeling/drivers.mdx (via TursoDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via TursoDriver (symbol, a top-level class))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/drivers/driver-turso/README.md) — pages documenting those are invisible to this run
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 133 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 455d0372f033f0085ffe1ed125033e59182a859fpackageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 455d0372f033f0085ffe1ed125033e59182a859f

⚠️ 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 455d0372f033f0085ffe1ed125033e59182a859f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…r, not patch (#16024)

The Check Changeset step's WHICH LEVEL rule (maintainer, 2026-09-04, batch #35):
a purely additive widening of a published package's public surface takes at
least minor, and the act sets the floor. The new D3 entry in
packages/spec/src/migrations/registry.ts is that act for @objectstack/spec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Test Core (5/6) did not fail — it timed out, and the measurement says why

PM (domain:engine dispatching seat), 2026-09-06T17:1xZ. Recording this once, per the drive-to-green rule, so the blocker is on the PR rather than only in a dispatch log. Head re-read at the moment of writing: 8c3dd7b3, unchanged — so this is ⛔ not a superseded run.

What the job actually did

step result
Set up job → Install dependencies → Compute this shard's package set success (all of them)
Run this shard's tests cancelled
Publish turbo run summary · Test completeness guard · Post Checkout success
Attest this shard ran and passed skipped

The log ends ##[error]The operation was canceled. mid-stream through @objectstack/cli's suite, with no Failed Tests block, no Test Files … failed, and no ELIFECYCLE. ⇒ nothing asserted false. The roll-up Test Core reports failure because shard 5 produced no attestation, not because it produced a failing one.

Why it is a timeout, and why it is structural rather than a flake

PR Test Core (5/6) duration
this PR cancelled 30.3 min
#16057 success 25.7 min
#16363 success 25.2 min
#16326 success 20.1 min

30.3 minutes against a 30-minute job ceiling, with every sibling landing in 20–26. ⭐ And the siblings are not comparable work: they touch packages/core / packages/objectql only, while this PR touches packages/spec, which invalidates the Turbo cache for everything downstream — so this shard rebuilds far more before it runs a single test. The extra minutes are bought by the rebuild, not by slower tests.

"Flake" is not the right word and I am not using it. A flake is a real run with a nondeterministic result; this is a run that never finished. The margin (0.3 min over) makes a warm-cache re-run plausible, which is precisely why a green re-run would prove nothing about the underlying fit.

What I am doing, and what I am not

  • Re-running the shard once — the permitted single re-run for a job that died before its test body completed.
  • Not skipping, quarantining or narrowing any test to fit the window. ⛔ Not pushing an empty commit. ⛔ Not widening this PR to "fix CI".
  • If the re-run also times out, this is not something this PR can resolve from inside itself: a spec-touching change does not fit shard 5's current ceiling, and that is a CI-capacity question for whoever owns the sharding — I will say so and leave the PR watched rather than hand it to anyone.

Everything else on this PR is green: 37 checks, 0 failing apart from the shard and its roll-up. needs:contract-review remains hung on both carriers and the PR stays a draft; ⛔ nothing here is a release.


Generated by Claude Code

@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

The re-run timed out at exactly the same 30.3 minutes. This PR cannot fix it from inside itself.

PM (domain:engine dispatching seat), 2026-09-06T17:4xZ. Head re-read now: 8c3dd7b3, unchanged — same head as the first attempt, so this is ⛔ not a supersession.

attempt Test Core (5/6) duration
first cancelled 30.3 min
re-run (the one permitted) cancelled 30.3 min

Identical to the tenth of a minute. That is not variance around a boundary — it is a hard ceiling being hit twice by work that does not fit under it. The comparison stands: three sibling PRs pass this shard in 20–26 min, and they touch packages/core / packages/objectql only, while this PR touches packages/spec and invalidates the downstream Turbo cache, so the shard rebuilds far more before running a single test.

Current state at this head: 37/37 completed, 2 failing — the shard and its roll-up, the roll-up only because a cancelled shard publishes no attestation. Nothing in this PR asserts false; every other check is green.

⛔ What I am not doing, and why

  • No further re-run. One was permitted for a job that died before its test body completed; it is spent, and a third attempt would be hoping for a different answer from an identical measurement.
  • No skipping, quarantining, narrowing or sharding-by-hand of any test to fit the window.
  • No empty commit, no close-and-reopen.
  • Not widening this PR. The change is the ADR-0049 disposition the maintainer ruled per key; the shard's time budget is not part of it.

What this actually is

A CI-capacity question owned by whoever owns the sharding, not a defect in this diff. Filed as a finding so it is tracked rather than rediscovered by the next packages/spec PR — which will meet the same ceiling, and for which it will equally not be that PR's problem.

⚠️ Correcting my own earlier note on this PR: I reported at 17:3xZ that the re-run had cleared it. It had not — I read 35/36, 0 failing while the re-run was still in flight and treated a transient state as an outcome. completed == total AND failing == 0 is the condition, and 35/36 is not completed == total. The reading was mine to get right and I did not.

This PR stays a draft with needs:contract-review hung on both carriers, watched, ⛔ not handed to anyone.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ADR-0049] @objectstack/driver-turso publishes three config keys nothing reads — timeout on the TS interface, localPath and wasm on the zod schema

1 participant