Skip to content

fix(driver-turso)!: refuse timeout beside a wss:// / ws:// url at construction (ADR-0049 enforce-or-remove) - #16616

Merged
os-musk merged 3 commits into
mainfrom
claude/issue-16378-turso-ws-timeout-refusal
Sep 7, 2026
Merged

fix(driver-turso)!: refuse timeout beside a wss:// / ws:// url at construction (ADR-0049 enforce-or-remove)#16616
os-musk merged 3 commits into
mainfrom
claude/issue-16378-turso-ws-timeout-refusal

Conversation

@os-musk

@os-musk os-musk commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16378

What

TursoDriverConfig.timeout beside a wss:// / ws:// url in remote mode is refused at construction — VALIDATION_ERROR / 400, the ADR-0112 envelope, with a message naming the key, the scheme it met and both ways out — instead of being accepted and never delivered. This is option (c) from the card, as routed by triage; (a) routing WebSocket urls over HTTP, (b) a driver-side race and (d) leaving it documented are not taken.

Premise check — re-derived on this head

Installed from packages/drivers/driver-turso: @libsql/client@0.17.4 / @libsql/core@0.17.4 / @libsql/hrana-client@0.10.0 — the pinned versions, unmoved.

  • Scheme routing: @libsql/client/lib-esm/node.js sends wss / ws to the ws client and https / http to the HTTP client; the entry this driver imports expands libsql:// to HTTPS (@libsql/core expandConfig(config, true)).
  • lib-esm/ws.js: 0 fetch hits; it opens hrana.openWs(url, config.authToken). lib-esm/http.js forwards config.fetch into hrana.openHttp (lines 29, 50, 201).
  • @libsql/hrana-client/lib-esm/ws/*.js + lib-esm/index.js: 0 timeout hits. The control that fires: a fetch grep over lib-esm/http/ hits client.js 33/46/51/87/114/118 and stream.js 21/37/245/246. Note: the card's control as literally worded — a timeout grep over lib-esm/http/ — also returns 0 on this head; the control that makes the zero a reading is the fetch grep. The conclusion is unchanged.
  • TursoDriver.detectMode accepts wss:// / ws:// as 'remote' (pinned in turso-driver.test.ts), and after PR feat(driver-turso)!: timeout bounds remote operations; localPath and wasm leave the published config schema (ADR-0049 enforce-or-remove) #16376 the window is applied only inside createRemoteClient (fetch: fetchBoundedBy(timeoutMs)), which the WebSocket client never reads. A wss:// url with timeout: 30000 constructed, connected and ran unbounded on this head. Premise valid.

Where the refusal sits

In the constructor beside detectMode, before super() — ahead of the Knex base and of any client, so it cannot be reached with a half-built driver. Both loaders (@objectstack/runtime's host factory and the open-core datasource factory) reach it through buildTursoDriverConfig then new TursoDriver, with no catch in between, so a datasource authoring timeoutMs beside a WebSocket url now fails its connect by name. Scoped to remote mode: on the replica arm sync() is bounded whatever the url's scheme; timeout: 0 is the documented "no bound" and is not refused; a caller-supplied client is not consulted (its transport is not the driver's to know — see acceptance notes).

Surface repaired

The docblock, README and docs/design/driver-turso.md lines PR #16376 wrote ("not bounded") now say "refused" — "not bounded" and "refused" are different promises. TursoConfigSchema (this package's mirror) and @objectstack/spec's strict turso contract are untouched: neither makes a WebSocket promise, so nothing there went stale.

Clause-②: yes

Re-derived from the diff: the constructor newly refuses a configuration it accepted on origin/main — an accept-set narrowing on a published driver option. Changeset @objectstack/driver-turso: minor, BREAKING in the body under the launch-window convention, ADR-0087 disposition not-required (no-migration-prescription) — nothing is retired or renamed; the refusal text names both ways out. needs:contract-review is hung on the card and on this PR together.

Evidence

Package @objectstack/driver-turso; every build and test run went through scripts/pm/os-verify-lock.sh (shared box — durations are not idle-box figures).

  • Closure build turbo run build --filter='@objectstack/driver-turso^...': 7/7 tasks, exit 0. Package build: exit 0 (check-dts-emitted 1/1).
  • tsc --noEmit --listFiles: exit 0; the new pin file is in the program (1 hit).
  • Targeted: vitest run src/turso-driver-ws-timeout-refusal.test.ts src/turso-driver-timeout.test.ts src/turso-driver.test.ts src/spec/turso.test.ts — 4 files, 146 tests, exit 0.
  • Full package suite at the merged head 94d13f601d: 46 files, 1189 tests, exit 0 (35.7 s).
  • Spec moved on main's side in the merge: pnpm --filter @objectstack/spec build exit 0; check:generated 15/15 artifacts up to date, exit 0.

Reverse verification — direction predicted before it ran: refusal cases RED, controls GREEN

turso-driver.ts restored tree-only from BASE ac9376a74: anchor refuseWebSocketTimeout count 4 to 0, mutated blob 84c634d1… equal to the BASE blob. Pin run under the mutation: 4 failed | 7 passed, exit 1 — the four refusal cases red (the constructor returned a driver, transportMode: 'remote'), all seven controls green. Restored with git checkout HEAD -- path: count back to 4, blob 5c2ff12f… equal to the HEAD blob, git diff HEAD empty, git status --porcelain empty. The pin resolves ./turso-driver.js to in-package source, so no dist/ leg applies to this ablation.

Gates — derived on the merged tree 94d13f601d, 57 commands; --ran: 57 derived, 57 run, 0 unrun

  • 53 exit 0 on the first pass. check-adr-0087-registration --base origin/main was red on the first changeset draft — it read a two-column way-out table as a migration prescription contradicting no-migration-prescription — rewritten as prose, committed (6acaf013e4), re-run: exit 0. check-changeset-no-major, check-empty-changeset and check:nul-bytes re-run on the rewrite: exit 0.
  • NOT MEASURED, in the gates' own words (PREREQUISITE NOT MET, exit 3), left to CI which builds the closure first: check:doc-formula-expressions (@objectstack/lint not built), check:dual-build-cjs-loads (a package with no dist/), check:type-check-debt (unbuilt type closure). Per the PM's correction: no heap raise, no whole-workspace build.

Acceptance notes

  • noted, not filed: the card's literal positive control (timeout grep over hrana lib-esm/http/) returns 0 on this head; the firing control is the fetch grep. Same conclusion.
  • filed separately: a caller-supplied client in remote mode leaves timeout reaching nothing — the window is applied only by createRemoteClient, which both remote client sites skip when client is set. Same ADR-0049 shape, one composition over; outside option (c).
  • p2 escalation question (triage): no in-repo deployment, example, doc or env sample pairs a WebSocket Turso url with a window, and the OS_DATABASE_URL boot path forwards only url and authToken, so an env-configured deployment cannot carry timeout at all. That is a reading about this repository, not evidence about external deployments.
  • service-datasource still reads the retired turso config.timeout, so a datasource authored with the canonical timeoutMs is dropped at the seam that builds the driver config #16023 remains open and is not touched here (the datasource seam's reading of the retired config.timeout).

Generated by Claude Code

…struction (ADR-0049 enforce-or-remove)

A non-zero TursoDriverConfig.timeout beside a WebSocket url in remote mode
reached nothing (@libsql/client 0.17.4's ws client takes no fetch and no
timeout option); the constructor now refuses the pair as VALIDATION_ERROR / 400
before super(), naming the key, the scheme and both ways out. Controls pin the
width: no timeout, timeout: 0, every HTTP-side scheme with a window, and the
replica arm stay accepted. Docblock, README and design-doc lines that promised
"not bounded" now say "refused".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
…er entry — nothing is retired)

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

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 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.

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
  • 1 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 — 6 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 a7c14cdf371bdb0f22be28e47a40fab9f7be88d0packageMentionDocs.

Which tree this was computed on

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

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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

os-musk commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Clause-② cleared → ready → auto-merge armed 14:34:55Z

domain:engine PM dispatch seat, session session_01ADLdAs2pVcH17h9tZKWMBg, R18.

Provenance. In-seat contract review returned PASS and is recorded verbatim on the card: objectstack-ai/objectstack#16378 (comment 5572144928). Isolated review subagent, model: fable, fed the card + rulings + this PR only — ⛔ not the dispatch order, ⛔ not this seat's conclusions.

Implemented-by: branch claude/issue-16378-turso-ws-timeout-refusal (mode:subagent dev of session_01ADLdAs2pVcH17h9tZKWMBg)
Reviewed-by: context-isolated review subagent of session_01ADLdAs2pVcH17h9tZKWMBg

Tier verification (contract-review.md:56, the hard door before adoption): transcript grep of the harness-stamped model field → 132 / 132 claude-fable-5-1, zero other values; control: 115 assistant records in the same file. ⛔ No fallback evidence ⇒ the verdict is adoptable in full. ⛔ Adopted verbatim; a parent session's only two lawful moves are verbatim adoption or wholesale voiding.

needs:contract-review stripped from both carriers in one stroke — card #16378 and this PR.

Landing pre-checks — stated as measured, including the one that is not clean

# check reading
in-seat contract-tier PASS on record ✅ comment 5572144928
both carriers cleared, machine-read ⚠️ PARTIAL — ⛔ not reported as clean. See below.
every check green, ⛔ not the required subset ✅ 40 check runs on 6acaf013e4, every conclusion success or skipped, 0 failed. Check Changeset (run 34133526153, re-fired by the label strip at 14:32:43Z) completed success before this flip.

② in full, because "partial" without the shape is worthless. check-clause2-carriers.mjs --pair 16616 was run on both of its routes:

  • live → exit 3, PREREQUISITE NOT MET, GET /repos/…/pulls -> HTTP 403, in its own words "0 pair(s) had been read when it failed, so this run is NOT a reading of a clean board". That is this session's known channel fact (direct REST is 403 for this seat; MCP only) — ⛔ a fact about the caller, not about the board. ⛔ Exit 3 is not clean and is not being read as clean.
  • offline, via the script's own network-free route --pair-json with a pre-fetched pair → exit 2, INCOMPLETE, and the gap is named exactly: "card TursoDriverConfig.timeout has no seam on the WebSocket transport (wss:// / ws://) in @libsql/client 0.17.4 — documented as unbounded, not delivered #16378's comment thread could not be read". ⚠️ ⛔ An empty comments array was not supplied to close it — the script's own header warns that a hand-assembled input must not manufacture a clean reading out of a gap, and fabricating that array is precisely that.

⇒ What the offline run did establish: it served 3 reads and its sole complaint was the comment thread — ⛔ not a label disagreement, which is what exit 4 would have been. So the label limbs were read and are consistent. The unread channel is the comment/event corroboration, not the carriers themselves.

Substituted instrument, stated so it can be checked rather than trusted: both carriers' label sets read back directly after the strip — card #16378 bug, pm:dispatched, domain:engine, priority:p3; this PR documentation, size/m, tests, tooling. needs:contract-review absent from both. The reviewer independently read both sets before the strip (§6 of the verdict) and found it present on both, so the transition is witnessed from both sides.

Governed-surface probe — 0, with a firing control

git diff --name-only 434ca2d64d…refs/pull/16616/head | grep -cE '^(docs/adr/|\.claude/|skills/|AGENTS\.md$|CLAUDE\.md$)'   → 0
                                          control:   | grep -cE '^docs/'                                                 → 1

⇒ the one docs/ file is docs/design/driver-turso.md, and docs/design/** is ⛔ not docs/adr/**. Five files, none governed ⇒ the ordinary ready → auto-merge → queue path applies. ⚠️ Recorded with the control because a zero-hit grep is not a result without one.

Carried forward, ⛔ not ridden on this PR

The review measured one corner the refusal does not reach: @libsql/core's expandConfig lowercases the scheme before routing (config.js:26), while ridesWebSocketTransport and detectMode are case-sensitive — so { url: 'WSS://…', mode: 'remote', timeout: N } still constructs with an undelivered window. It is narrower than the gap, not wider, so it is not a defect in this diff. A follow-up card is being filed beside #16617; ⛔ it is not a rider here.

⚠️ Also carried: CI ran on merge commit 532de85b against base a7c14cdf, and origin/main has since moved to 8341ed21. The merge queue re-runs against the real queue head, which is the reading that decides entry — ⛔ not this head's green.

PM dispatch seat · domain:engine · session session_01ADLdAs2pVcH17h9tZKWMBg · R18


Generated by Claude Code

@os-musk
os-musk added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit bc0ac1d Sep 7, 2026
42 checks passed
@os-musk
os-musk deleted the claude/issue-16378-turso-ws-timeout-refusal branch September 7, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TursoDriverConfig.timeout has no seam on the WebSocket transport (wss:// / ws://) in @libsql/client 0.17.4 — documented as unbounded, not delivered

2 participants