KDB — Koder Database

  • Area: Data Platform
  • Path: infra/data/kdb (the kdb Rust component lives under infra/data/kdb/ during the transitional period — path move tracked in ticket #730a'..#730f)
  • Kind: Two-in-one (transitional) — observability DB legacy (Go, moving to infra/observe/kdb-obs-legacy/ per #730e) + general-purpose relational engine (Rust, the canonical kdb)
  • Status: field-test (since 2026-05-22 per #730a) — promoted from experimental because the Koder Stack has no users yet and the remaining gates (stability, production_proven) only close via adoption

The kdb name (2026-05-22 — ticket #730 reconciliation)

Per meta/docs/stack/registries/component-names.md, the canonical name of this component is kdb (display + bare + slug). The kdb-next label is the transitional name of the Rust rewrite while it coexisted with the Go kdb 1.x — that coexistence is ending. References to "kdb-next" in older docs and commits are historical; new docs use "kdb".

Two KDBs, one name — transitional split

Legacy Go kdb (moving to infra/observe/kdb-obs-legacy/ per #730e)

The Go engine historically at infra/data/kdb (Go binary koder-kdb + kdb-soak) is a specialized multi-model database for the Koder Observability platform. It ingests diagnostic data from koder-traced agents and serves queries to the observe/observability dashboard.

The Go binary's SQL/KV path never reached production (per KMCP-003.1 autonomous probe 2026-05-22 of s.khost1) and will be archived. The Go binary's observability subset (Prometheus scraper, alerting, log aggregation, agent registry) moves to infra/observe/kdb-obs-legacy/ — it's a service of observability that historically lived next to kdb, not a property of kdb itself.

Features (legacy Go obs subset):

  • Data Ingestion — REST API for metrics, ping, HTTP, DNS, traceroute, port, MTR, browser timing, and system metrics
  • Query Languages — KQL (Koder Query Language), PromQL, GraphQL
  • Alerting Engine — Rule-based, threshold + duration
  • Health Scoring — Per-site score (0–100) from multiple dimensions
  • Anomaly Detection — Z-score based, with trend prediction
  • Grafana Compatibility — Prometheus remote writeread, `metrics` exposition
  • Retention Management — Automatic cleanup

kdb (Rust, canonical) — General-purpose relational, hyperscale

The Rust kdb, defined in RFC-001 and currently living under infra/data/kdb/ (path move tracked in #730a'..#730f), is a from-scratch relational engine targeting 100M+ tenants on a TiKV substrate with Raft consensus. It is the substrate for every Koder SaaS that needs relational storage — Flow (Phase 7.1 done), Talk (Phase 7.2 in progress), Kortex (Phase 7.3 pending), and beyond.

Current version: v0.1.0 — soft-promoted to field-test 2026-05-22 (#730a, version bump from 0.0.14). Built on v0.0.13 performance wave (#676, #680, #683, #685, #686, #690, #699 + admin closures), v0.0.12 RFC #548 god-file decomposition, and v0.0.11 RFC-009 active-active observability + RFC-012 vectorgraph substrates + multi-table 2PC fix. Phase 7 migration of koder-flowkoder-talkkoder-kortex from Go kdb to kdb as primary is in progress; 7.1 (flow flip 2026-05-04) done, 7.2 (talk flip) and 7.3 (kortex activation) tracked under tickets #547 and #545. Bench v0.0.13 dev smoke: large_scan 4.4× opss, p99 6.8× faster vs v0.0.12 (1.32× pg-faster, gate satisfied).

Disk format break window: until 2026-06-30, the on-disk format may change without a migration tool — no user data exists to lose. After that date, format changes require a migration tool.

Client SDKs — all standard PostgreSQL drivers work natively. kdb-specific multi-tenant wrappers:

Language Package Location
Go go.koder.dev/kdb (pgx/v5 wrapper) infra/data/kdb/sdk/go-pgwire/
Python kdb-next (asyncpg + psycopg2) infra/data/kdb/sdk/python/
JavaScript/TS @koder/kdb (node-postgres wrapper) infra/data/kdb/sdk/js/
Ruby/Rails kdb-next gem (pg + ActiveRecord adapter) infra/data/kdb/sdk/ruby/

Recent changes

kdb-pgwire — Describe(portal) now reports the Bind-negotiated result formats; fixes pgx-default INT8 scan (kdb#810, 2026-06-24, /k-go)

Describe(portal) built its RowDescription with an always-text format code (do_describe_portaldescribe_sql_fieldsmake_field), ignoring portal.result_column_format. But the matching Execute encodes the DataRows in the Bind-negotiated format (binary for INT8 etc.). A pgxgo-pgwire client in the default exec mode reads the Describe(portal) RowDescription to pick its scan plan (pgconn.readUntilRowDescription: when a RowDescription is present pgx trusts it over the formats it requested in Bind) — so it text-decoded the binary i64 bytes and failed (strconv.ParseInt on vec_idrecall_count). simple_protocol (all-text) sidestepped it; that's why only the extended path broke.

Fix: new response_builders::describe_portal_fields(state, sql, &Format) re-stamps each column's wire format from formats.format_for(i) — the same portal.result_column_format SSOT the Execute encoder uses — so Describe and Execute agree by construction. do_describe_portal uses it; do_describe_statement keeps text (correct per protocol — formats are unknown before Bind). This was the actual root cause behind the #809-bench gap; the original #810 hypothesis (encoder/format_for) was wrong — the encoder was already correct.

Tests (off-laptop dev-linux-kdb): lib 439/0 incl. new describe_portal_fields_reflect_bound_result_formats (UnifiedBinary→all-binary, UnifiedText→all-text, Individual([1,0])→positional; statement-describe stays text). No regression: regression_binary_wire_format_416 (tokio-postgres binary) 1✓, pgwire_extended_query 5✓, pgwire_describe_param_types 1✓, regression_array_param_any_809 3✓. End-to-end: the services/ai/memory#808 bench (pgx default mode, fixed gateway) populated 2000 episodes + ran 500 recall queries EXIT=0 (48.9 q/s, p50 4.7ms) — the vec_id BIGINT scan round-trips. Closes kdb#809 criterion 3: the memory store now runs 100% on kdb-pgwire + kdb-vector.

kdb-pgwire — array-typed query params: col = ANY($1) describes as text[]int8[] + Bind decodes the slice (kdb#809, 2026-06-24, `k-go`)

An array-typed parameter used as col = ANY($1) described as unknown (OID 705), so pgxtokio-postgres refused to encode a host slice ("cannot find encode plan"). That blocked the `servicesai/memory store's batch **hydrate** (WHERE ulid = ANY($1)`) from running on kdb-pgwire — it had to fall back to stock PostgreSQL 17. Three slices close it:

  • Slice 1 — inference (handler/response_builders.rs): infer_predicate_params now recognises Expr::AnyOp (col = ANY($n)) and reports the column's array OID (text[]1009, int8[]1016, …) via the new types::data_type_to_pg_array; the explicit $n::text[] cast form is honoured too. Both Describe and Bind share this path, so a standard client gets a real array OID instead of unknown.
  • Slice 2 — decode (handler/params.rs): array OID constants + array_element_oid map; decode_text_paramdecode_binary_param detect an array OID and decode the Postgres text ({a,b,c}, with quoteescapeNULL rules) and binary (ndimflags/elem-oid header + per-element length-prefix) wire forms into Value::Array, decoding each element by its scalar OID. Malformed binary degrades to Value::Null (no panic).
  • Slice 3 — eval: no change needed — the planner's array_any already consumes a Value::Array, so once Bind yields the list, col = ANY($list) evaluates.

Tests (off-laptop dev-linux-kdb): kdb-pgwire lib 438/0 (9 array-decode + 3 inference unit tests), new regression_array_param_any_809 3/3 drives the full Parse→Describe→Bind→eval path through a real tokio-postgres client binding a Vec<String>Vec<i64> to col = ANY($1) (and the $1::text[] cast) — the exact memory-hydrate shape. Neighbouring pgwire_describe_param_typespgwire_extended_querypgwire_v2 stay green. Consumer confirmation (criterion 3 — re-running the memory#808 bench with --dsn on kdb-gateway pgwire) ran 2026-06-24: populate + the id = ANY($3) hydrate executed (OID-705 wall gone), but it surfaced a separate pre-existing gap — extended-query INT8 result columns are binary-encoded while the RowDescription says text, so pgx (default mode) text-decodes binary bytes (vec_idrecall_count scan fails; simple_protocol works). Filed as kdb#810 and fixed the same day (see the entry above) — criterion 3 is now closed (memory store 100% on kdb-pgwire + kdb-vector).

kdb-vectorkdb-gateway — metadata predicate pushdown into HNSW traversal (filtered-HNSW, kdb#807, 2026-06-24, `k-go`)

#806 made a filtered nearest correct by oversampling (max(k*10, 128) candidates, then post-filter). That pool under-fills when the k-th nearest matching vector ranks past the floor. #807 pushes the AND-of-eq predicate into the level-0 HNSW traversal (ACORN-style), so the search collects only matching nodes while still expanding through non-matching ones — it keeps going until it has k matches.

  • kdb-vector/src/hnsw/persistent.rs — new PersistentHnswIndex::search_filtered(query, k, metadata_filter); search() delegates with an empty filter. search_layer gained a predicate + visit_budget: the predicate gates collection (result heap) but not expansion (a non-matching node can still bridge to a matching region — the recall-correctness property). The node's metadata is already resident in the per-request VectorCache from the distance read, so the test is zero extra IO. A bounded visit budget (128×ef, floor 16 384) caps a selective predicate from degrading into a full graph walk; the unfiltered path stays unboundedunchanged.
  • search_strategy.rs — the planner picker no longer forces a filtered query to linear (HNSW now honours it); it weighs only index presence + corpus size. The #754 size rule still steers small corpora to exact linear. Selectivity-aware routing (send pathologically selective filters to exact linear) is deferred to the cost-based picker (#723).
  • kdb-gateway/src/vector_service.rsnearest makes one search_filtered call; the oversample + post-filter block (and the gateway-local filter_matches) are gone.
  • Recall verdict (deterministic, vs linear ground truth): 1%-selectivity over N=2000 returns the true k-nearest matching exactly. The property is graph-collection-invariant (N-independent); a full 100k recall+latency bench is a perf-soak follow-up, not a correctness gate. Note: an HNSW graph can leave a far outlier minority unreachable (back-edges pruned by the dense majority) — orthogonal to filtering, affects post-filter and pushdown alike.

Tests (off-laptop dev-linux-kdb): kdb-vector lib 95/0, gateway lib 391/0, new regression_vector_filter_pushdown_807 (matches found past the 128 oversample floor on a connected corpus) + regression_vector_nearest_filter_806 stays green.

kdb-gateway — sled write-death auto-recovery via systemd watchdog + external stall alert (KDB-805, 2026-06-22, /k-go)

Forensic hardening after the 2026-06-21 incident: a transient ENOSPC on the host pool wedged the gateway's sled iobuf write-dead for ~13h with no auto-recovery (every PromQL query over kdb-ts returned empty; the DASH-009 evaluator was blind) until a manual systemctl restart. In-process sled reopen is unsafe (the Arc<sled::Db> is cloned across in-flight tasks); the proven recovery is a clean process restart, now made automatic + bounded:

  • src/write_health.rsWriteHealth (lock-free atomics) + sdnotify watchdog. READY=1 at boot; WATCHDOG=1 pings withheld under sustained append failure (≥20 consecutive errors and no success for ≥60s) → systemd restarts the unit (reopens sled clean). Idle never degrades; a blip recovering in <60s never restarts. No-op without `NOTIFYSOCKET`.
  • prom_serviceGET /ready (503 write-degraded, served from the in-memory flag, no TSDB query) + metrics kdb_ingest_write_errors_total / kdb_ingest_last_success_timestamp_seconds.
  • deploy/systemd/kdb-gateway.serviceType=notify + WatchdogSec=120 + Restart=always.
  • infra/observe/.../kdb-ingest-stall-alert.{sh,service,timer} — external write-death guard (mirrors MON-020 koder-disk-alert): scrapes /ready, posts straight to koder-notify, independent of kdb-ts (the DASH-009 evaluator can't alert on kdb's own death — circular). The path that would have caught the 13h outage in ≤1min.

Tests: write_health 44, prom_service 2121. Built release off-laptop (dev-linux-kdb); deployed + verified live on observe (watchdog armed, /ready+metrics live, sidecar E2E delivered firing+resolved to ops). Commit 6baa344d37.

kdb-commitkdb-pgwire — group-commit decouple widened to auto-commit UPDATEDELETE; #790 CLOSED (RFC-021 slice 2d, kdb#790, 2026-06-09, /k-go)

The final slice of #790 — and the close of the whole "make UPDATE/DELETE WAL records apply-order-independent" epic. The #789-β group-commit decouple (append WAL under the substrate lock → release → coalesced fsync off-lock → re-acquire → apply) previously admitted INSERT-only commits (writes_are_insert_only), because a decoupled UPDATE/DELETE's whole-table replace_all read the append-time physical state and a concurrent committer interleaving in the lock-release window could stomp it → primary/replica divergence. Now that ReplaceAllDelta's WAL records are the self-contained RowId-keyed v2 records (2a.3-ii-2) and its substrate apply is per-row at the stable id (2c), it commutes under the decouple. New kdb_commit::writes_are_decouple_safe admits Append + ReplaceAllDelta + Tombstone; the auto-commit path (dml_intercepts, txn_id == 0) uses it, so auto-commit UPDATE/DELETE now take the decoupled (coalesced-fsync) path. The explicit-txn path keeps the narrower writes_are_insert_only gate: there a Tombstone writes real conflict markers at apply time, and decoupling widens the FCW/SSI race window (a peer's pre-WAL conflict check runs before this txn's tombstone is applied) — closing that needs a fencedquorum gate (#418#781), out of scope. Bare ReplaceAll (TRUNCATE/MERGE, append-time diff + v1 records) stays excluded.

Regression auto_commit_update_decouples_and_converges_with_replica: N concurrent auto-commit UPDATEs on disjoint rows engage the decouple (the coordinator's durable_lsn advances PAST the seed — the inline path's wal.sync() never touches it), then a fresh replica replaying the WAL converges with the primary's exact final state (the under-decouple convergence guarantee). Plus writes_are_decouple_safe unit tests. Additive — no wire-format change. Green: kdb-commit / kdb-pgwire 460 / kdb-gateway / kdb-jepsen durability 37 (lone v2_batch_insert_1000_rows_timing load-flake passes isolated). This closes #790 + 789 slice 2b — UPDATE/DELETE replication is now correct AND gets the group-commit perf win. RFC-021 §7 increments 2a.1→2d all shipped.

kdb-commitkdb-walkdb-gatewaykdb-pgwire — the v2 WAL wire-format cutover: records carry the durable RowId (RFC-021 slice 2a.3-ii-2, kdb#795#790, 2026-06-09, /k-go)

The irreversible flip that makes the WAL carry RowId end-to-end (closes #795). append_commit_records now emits the RowId-keyed v2 records — InsertV2 (from BufferedWrite::Append, carrying its eager-allocated row_id), DeleteV2 / UpdateV2 (from ReplaceAllDelta deltas, by d.row_id) — superseding the interim v1 InsertDeleteUpdate whose format!("{row:?}") key was content-not-identity (duplicate rows collapsed; apply was order-dependent). Bare ReplaceAll (TRUNCATE / MERGE, no per-row id) keeps v1. KWAL_VERSION 1→2, with the segment-version check relaxed from != to > KWAL_VERSION — "new reads old": a v2 binary still replays v1 segments, while a pre-v2 binary (whose check is still != 1) fail-closed-rejects a v2 segment rather than mis-decoding the unknown variant indices. Stream parity gate: SubscribeRequest gained wal_format_version; the WalStream server rejects (fail-closed, FAILED_PRECONDITION) any subscriber whose declared version is below the version it emits — the stream ships bincode WalRecords per entry, so a pre-v2 replica would mis-decode or silently skip v2 records → divergence (always-on.kmd §2 / RFC-008 §4 same-build). The replica + recover_from_wal v2 arms (from 2a.3-ii-1) apply by the durable id; the 2c stable-id substrate apply keeps that id from churning — so the replica converges.

Closes the slice-1 content-key gap: duplicate-row Delete/Update is now unambiguous. Regressions: recover_v2_duplicate_rows_delete_is_unambiguous (two byte-identical rows at distinct ids; DeleteV2 one → the twin survives — RED vs the debug-key path, which would drop both) + auto_commit_update_delete_emit_per_row_wal_records (now counts InsertV2UpdateV2DeleteV2) + the existing v2-apply/recover tests. Real 2-process replication smoke PASS (scripts/776-replication-e2e-smoke.sh): primary BEGIN;INSERT×3;COMMIT; → emits v2 → replica subscribes at v2 (parity gate passes) → applies by RowId → converges with all 3 rows. Green: kdb-wal / kdb-commit 144 / kdb-pgwire 459 / kdb-gateway 323 / kdb-jepsen durability 37 (the 2 failures = v2_batch_insert_1000_rows_timing load-flake + subscribe_registers_… gRPC timing flake, both pass isolated). RFC-021 is now implemented through §4; only #790 slice 2d (decouple-gate widening — perf, not correctness) remains.

kdb-commitkdb-pgwire — FCW conflict gate moved before the WAL append (no orphan WAL on a doomed txn) (RFC-021 slice 2c-FCW, kdb#790, 2026-06-09, `k-go`)

A transaction that aborts with 40001 serialization_failure no longer leaves a durable WAL record. The SSI/FCW write-conflict check used to run inside apply_writes_2pc, i.e. after commit_txn had already appended and fsynced the Begin…Commit records (and advanced the WAL horizon) via wal_precommit_writes / wal_append_writes. A 40001 abort there left a durable WAL record for a transaction the primary rolled back — so recover_from_wal (crash recovery) or a WAL replica would replay a txn that never committed → divergence (the

790 bug class, on the explicit-txn path; pre-existing, independent of v1/v2 record

format). Extracted the check into kdb_commit::check_write_conflicts and call it in commit_txn before the WAL append on both the inline and group-commit-decouple paths; removed it from the apply (re-running it post-WAL would reopen the hole on a check↔apply race). On the single-node LocalKv primary the post-gate substrate apply is infallible redo, so write-ahead durability holds (WAL fsynced before apply) without the orphan hazard. KdbQueryHandler::get_wal_lsn made pub (the pg_current_wal_lsn() analogue) for the regression. Additive — no wire-format change. RED/GREEN regression kdb-jepsen::durability::phase_c_v3c_fcw_aborted_txn_leaves_no_wal_record (--features durability): TX_B aborts 40001 → the WAL horizon must NOT advance. Green: kdb-commit / kdb-pgwire 458 / kdb-gateway / kdb-jepsen durability 37 (incl. the existing 40001 + conflict-check-counter tests — the counter still bumps once per table, now from the pre-WAL gate). With 2c-FCW + 2c id-stability both done, the v2 WAL cutover (2a.3-ii-2) is fully unblocked.

kdb-adapterkdb-commitkdb-pgwire — per-row stable-id substrate apply for UPDATEDELETE (RFC-021 slice 2c id-stability, kdb#790, 2026-06-09, `k-go`)

Unblocks the v2 WAL cutover's id-churn divergence (the #795 finding). The ReplaceAllDelta substrate apply no longer wipes the table and re-allocates every row's id (whole-table replace_all_in_tx); it now applies each per-row delta at its stable durable RowId: DELETE drops the primary row at the id; UPDATE overwrites it in place (same id); rows not in the change set are left untouched — keeping both their id and their original MVCC xmin (the whole-table path wrongly re-stamped even unchanged rows with the committing txn). New 2b in-tx primitives RecordAdapter::{delete_at_row_id_in_tx, update_in_place_at_row_id_in_tx{,_with_mvcc}} (primary-key write only; substrate secondary indexes rebuilt post-commit by rebuild_secondary_indexes, the unchanged replace_all_in_tx contract). Same-txn UPDATE→UPDATE and INSERT→UPDATE chains carry real ids because AdapterWithSeqs::scan_with_row_ids now applies the buffered deltas to the substrate-with-ids view (preserving ids) instead of flattening to the id-0 new_rows post-state — so a chained statement's mutation capture addresses the real row, not the 0 sentinel. COMMIT PREPARED (XA, local-only, no WAL) deliberately stays whole-table. Additive — no wire-format change (producer still emits v1). Regressions: kdb-adapter::test_update_in_place_and_delete_at_row_id_in_tx (id preserved across update, delete drops exactly one, unchanged untouched) + kdb-pgwire::v2_same_txn_update_chain_applies_per_row (UPDATE×2 same row + INSERT→UPDATE + DELETE + untouched in one txn → correct final state). Green: kdb-adapter 42 / kdb-commit / kdb-pgwire 458 / kdb-gateway 324 / kdb-jepsen 16 (MVCC/SI unaffected by the unchanged-row re-stamp removal).

Remaining on #790 slice 2: (i) 2c-FCW — run FCW conflict detection before the WAL append on the explicit-txn path (a doomed 40001 txn currently fsyncs its WAL before the abort → a replica would apply a rolled-back txn; pre-existing, affects v1 too); (ii) 2a.3-ii-2 — now unblocked: emit v2 + KWAL_VERSION bump + parity gate (irreversible); (iii) 2d — widen the decouple gate + convergence test.

kdb-adapterkdb-commitkdb-pgwire — eager RowId allocation base + v2-cutover blocker found (RFC-021 slice 2a.3-ii-1b, kdb#795, 2026-06-09, /k-go)

Turned #795's resolved eager-allocation design into tested code, additively (no wire change): BufferedWrite::Append now carries an eager-allocated durable RowId (Append(String, u64, Row)); the buffered (explicit-txn) INSERT path reserves it at DML time (RecordAdapter::alloc_row_ids_for_table); AdapterWithSeqs::scan_with_row_ids surfaces that real id for buffered rows — resolving the 2a.2 0 sentinel, so a same-txn INSERT→UPDATE/DELETE captures the right RowDelta.row_id. The commit apply lands the row at the pre-assigned id via new in-tx primitives RecordAdapter::append_at_row_id_in_tx{,_with_mvcc} (mirror append_in_tx{,_with_mvcc}, id passed in not allocated); COMMIT PREPARED replay uses append_at_row_id. Substrate-identical to the old path (same allocator/order; the allocation moment moved earlier; gap-tolerant allocator covers abort). The producer still emits v1 — zero divergence risk. Regression kdb-adapter::test_eager_alloc_and_append_at_row_id_in_tx. Green: kdb-adapter 41 / kdb-commit / kdb-pgwire 457 / kdb-gateway 324 (lone v2_batch_insert_1000_rows_timing = documented load-flake, passes isolated).

⚠️ Finding — the v2 producer cutover (2a.3-ii-2) is BLOCKED on the per-row stable-id substrate apply (#790 slice 2c). Grounding the commit apply showed emitting v2 is not independently correct: ReplaceAllDelta's substrate apply still re-allocates ids (replace_all_in_tx), so after the 2nd UPDATE of a row the v2 UpdateV2{row_id} addresses an id the replica lacks → no-op → data divergence (the #790 bug class). Cutover hard-depends on per-row stable-id substrate apply landing first (inseparable for correctness, D12). Corrected order: per-row stable-id apply (additive) → THEN emit v2 (irreversible). Detail in kdb#795.

kdb-walkdb-gatewaykdb-pgwire — v2 RowId-keyed WAL records, consumer side (RFC-021 slice 2a.3-ii-1, kdb#795, 2026-06-09, /k-go)

The consumer half of the WAL-RowId bump, landed in the safe readers-before-writers order (always-on.kmd §2). WalRecord gains InsertV2 { row_id, row_data } / DeleteV2 { row_id } / UpdateV2 { row_id, new_row_data }appended at the end of the bincode enum so the v1 variant indices (0..=8) are untouched and existing on-disk segments decode byte-identically ("new reads old"). ReplicaSink gains apply_insert_at_row_id / apply_delete_by_row_id (the prod RecordAdapter delegates to the 2a.3-i primitives; the in-memory test sink is best-effort). Both replica apply paths — the gateway WalApplier (BufferedOp + apply/flush) and recover_from_wal — gained v2 arms that apply by the durable RowId (a direct keyed op: InsertV2UpdateV2append_at_row_id, DeleteV2UpdateV2-old → delete_by_row_id), replacing the v1 O(table) debug-string scan-filter and making apply duplicate-safe + order-independent. The producer still emits v1 — nothing emits v2 yet, so this carries zero production risk. Strong regression recover_replays_v2_rowid_insert_update_delete (synthetic v2 WAL → fresh real-adapter replica → InsertV2(id10,id20) + UpdateV2(id10→a2) + DeleteV2(id20) → only id10='a2' remains) + applies_v2_rowid_insert. Green: kdb-wal / kdb-commit / kdb-gateway / kdb-pgwire 457. Next: 2a.3-ii-2 — the gated producer cutover (emit v2 + resolve the row_id=0 buffered sentinel + KWAL_VERSION bump + stream parity gate + 2-proc smoke).

kdb-adapter — row-id-addressed substrate primitives (RFC-021 slice 2a.3-i, kdb#795, 2026-06-09, /k-go)

First sub-slice of 2a.3 (the WAL-RowId bump), landed after the owner chose path B for the 2a.3↔2b coupling (2a.3 owns the minimal row-id apply primitives; 2b generalises to in-place + MVCC). RecordAdapter::append_at_row_id(table, row_id, row) writes a row at a caller-supplied RowId instead of allocating a fresh one (Key::primary(tenant, table, row_id)), and delete_by_row_id(table, row_id) deletes the row at that id — both with the same secondary / vector / trigram / doc-GIN index maintenance as append/delete_rows, and delete_by_row_id is idempotent (returns false if absent → a replayed Delete is a no-op). These are the substrate ops a replicarecovery applier needs to landremove a row at the identity the primary assigned (carried on the v2 WAL), so the replica never re-derives the id. Additive — NO WAL/format change, no consumer yet (the v2 applier wiring is the gated 2a.3-ii). Regression test_append_and_delete_by_row_id (out-of-order ids 50/100 land + scan in PK order; delete-by-id removes exactly one + idempotent re-delete); kdb-adapter suite 40 green. Next: 2a.3-ii — the gated wire-format flip (WalRecord v2 + versioned envelope + producer cutover + applier wiring).

kdb-pgwirekdb-adapter — DML scan surfaces RowId → RowDelta.row_id (RFC-021 slice 2a.2, kdb#794, 2026-06-09, `k-go`)

Second slice of #790 slice 2 (after the 2a.1 durable allocator). The UPDATE/DELETE mutation path now captures each changed row's durable RowId into the per-row delta — the identity that 2a.3 will carry on the WAL and 2b/2c will address the per-row substrate apply by. RFC §3.3 option-b: a new RowSource::scan_with_row_ids → Vec<(u64, Row)> on the planner trait (surfaced as a storage-agnostic u64kdb-planner must not depend on kdb-record; the RowId newtype wraps it at the RowDelta layer), default-impl pairs scan rows with the 0 "no durable id" sentinel; RecordAdapter overrides it (the record scan already strips the key prefix, so the pk is the 8-byte component); AdapterWithSeqs overrides it with the same buffered-write merge as scan (buffered same-txn rows = 0 sentinel, hand-off to 2a.3); RowDelta gains row_id: kdb_record::RowId; update_rows/delete_rows populate it. Additive — the WAL is unchanged (still keys on the interim debug string); row_id is captured but not consumed until 2a.3. Green: kdb-adapter (+ new test_scan_with_row_ids_surfaces_durable_ids) / kdb-commit 972 / kdb-planner / kdb-pgwire / kdb-gateway 322. Next: 2a.3 (the gated WAL-format bump).

kdb-recordkdb-adapter — durable per-table RowId allocator replaces gen_pk (RFC-021 slice 2a.1, kdb#793, 2026-06-09, `k-go`)

First foundational slice of #790 slice 2 (RFC-021, owner-approved). The substrate primary key was an ephemeral, process-global surrogate: gen_pk() = a single next_pk AtomicU64 seeded from the boot nanosecond timestamp. That re-derives identity per process and can't be carried on the WAL — blocking per-row UPDATE/DELETE apply (the #790 source-#2 stomp). This slice promotes it to a durable, per-(tenant, table), monotonic RowId while keeping the on-disk shape identical (8-byte big-endian components under INDEX_PRIMARY — existing primary-index scanspagingsecondary layout are byte-compatible). Additive: no WAL/format change (that's the gated increment 2a.3). Realized as a dedicated SYS_NEXT_ROW_ID system table mirroring the existing SYS_ROW_COUNT durable counter (Catalog::allocate_row_ids, atomic RMW under a pessimistic KV tx + CAS-retry; seed-on-open from max(existing key)+1 for pre-existing tables) — not a CatalogTable field, sidestepping the EnsureTable strict-drift wall + catalog cache-coherence (the §6/#793-sanctioned contingency; reuse-first on the row-count idiom). gen_pk/next_pk removed; all 7 insert sites (auto-commit appendappend_batchreplace_all + *_in_tx{,_with_mvcc}) allocate from it (batches reserve a contiguous block in one RMW); append_in_tx{,_with_mvcc} now return the allocated RowId (the 2PC apply loop discards it until 2a.3 threads it onto the WAL). Allocation is gap-tolerant (own KV tx → a rolled-back insert just skips an id; standard sequence semantics). Green: kdb-record 584 / kdb-adapter / kdb-pgwire / kdb-gateway 322. Next: 2a.2 (scan surfaces RowIdRowDelta.row_id), then 2a.3 (WAL-format bump, behind wal_format_version).

kdb-gateway — FIRST real cross-region replication smoke (kdb#543 slice-5 evidence, 2026-06-09, /k-go)

Every prior multi-region validation (#774/#776) ran same-VM (2 processes, netem-approx RTT). This session ran the WAL-streaming path over the real internet between two real regions: primary = GCP koder-fleet-geo-us (us-central1-c Iowa, 34.56.109.210) ↔ replica = dev-linux-kdb (BR/EVEO), real RTT ≈ 139 ms. glibc gap (trixie 2.41 vs bookworm 2.36) solved by shipping a musl-static kdb-gateway (don't build on the 1 GB e2-micro). GCP firewall opened via the Compute API (the rpm32510 token has cloud-platform scope — pure-REST, no gcloud). Results: 18 rows written on the Iowa primary all replicated to the BR replica via the live subscribe-apply loop (6/6 probes, 0 timeout); partition tolerance (blackhole-route → replica stalls cleanly, no corruption) + catch-up after heal (≈2.3 s incl. stream re-establishment). End-to-end write→visible ≈0.85–1.5 s is client-RTT-dominated, not replication (steady-state replication is sub-second). kdb#543 slice-5 is no longer asset-blocked — the node + musl-ship recipe + Compute-API capability are proven; only the sustained soak + RYW-under-real-RTT + geo-failover RTO drill remain. Full recipe + caveats in the

543 ticket. (No product code changed — this is evidence.)

kdb-commitkdb-pgwire — UPDATEDELETE WAL records made apply-order-independent + recover replay (kdb#790 slice 1, 2026-06-09, /k-go)

789-β decoupled the commit fsync from the substrate lock only for INSERT-only commits: the ReplaceAll (UPDATE/DELETE substrate effect) WAL encoding diffed Deletes from an append-time scan and re-emitted the whole table as Inserts — order-dependent, so two concurrent committers replaying onto a WAL replica diverge. /k-arch (option 1, identity-keyed deltas) split the fix: the WAL-format + recovery half is slice 1 (this), the per-row substrate apply (so the decouple can extend to UPDATE/DELETE) is slice 2 (v3c, still open on #790).

Slice 1: AdapterWithSeqs::{update,delete}_rows now capture per-row (pre→post) / (pre→∅) deltas at DML-execution time into a new BufferedWrite::ReplaceAllDelta { table, new_rows, deltas } (replacing the bare ReplaceAll on the buffered + auto-commit-WAL paths; TRUNCATEMERGEON-CONFLICT keep emitting bare ReplaceAll). append_commit_records emits per-row WalRecord::Update{old→new} / Delete{pk} (identity = whole-row debug string,

491) — no append-time scan, no whole-table snapshot. recover_from_wal now

replays Delete + Update (the prior code skipped Delete — a documented recovery hole, now closed) and is pub (a real programmatic recovery entrypoint). Substrate apply, the #770 index mirror, secondary-index rebuild, the mvcc_tombstones_observed counter, and SI conflict markers are unchanged (new_rows drives the same whole-table substrate effect; the paired Tombstone writes still carry the counter/markers). The decouple gate stays INSERT-only → UPDATE/DELETE remain on the inline-fsync path (correct, no stomp); widening it is slice 2. Tests green on dev-linux-kdb (per-row emission: UPDATE emits 1 Update + 3 Insert, not the pre-#790 8 Inserts; fresh-substrate recover reconstruction; disjoint-update order-independent convergence at the applier). Full regression green: kdb-commit, kdb-gateway (323), kdb-pgwire (457 integration). Regression #1100.

kdb-obj — 2nd consumer (Koder Driveminio-go) + 3 S3-surface gaps fixed (OSS-010 slice 4, 2026-06-08, `k-go`)

Koder Drive already ships a generic minio-go S3 backend, so it consumes kdb-obj with zero product code — and proving interop against a strict standard SDK (minio-go) drove out 3 real S3-API-surface gaps (G1), each fixed: GetBucketLocation (minio-go connect-time), Last-Modified (the engine now tracks object write time — ObjectMeta.last_modified + ObjectMetaRecord.created_unix, backward-compatible meta-blob decode), and CopyObject (wired to engine.copy). The Drive minio-go interop test against the live kdb-obj passes end-to-end (PutStatGetSizeCopy/ List/Delete). kdb-obj now has 2 real Koder consumers integration-proven (Hub depot + Koder Drive) — G5 (≥2 consumers). Remaining for the row-18 flip: a production cutover (owner-gated) + G3 maturation (≥3 stable releases).

kdb-obj — first real consumer: Hub depot S3Storage backend (OSS-010 slice 3, 2026-06-08, /k-go)

A /k-arch/k-go deliberation chose wiring a real consumer over an empty soak (only a real consumer earns G5 + non-empty soak evidence). It first fixed one genuine gap: a durable DiskBucketStore in kdb-obj-s3 (per-tenant bucket markers on disk, restart-survivable) replacing the in-memory registry — without it a write-consumer's PutObject after a server restart would NoSuchBucket. Redeployed + proven live (PUT into the same bucket after systemctl restart → 200). Then the Hub depot S3Storage backend (products/dev/hub/depot/storage/s3.go): the depot Storage interface (StoreGetDeleteExistsServeFile) over the kdb-obj S3 endpoint via dependency-free stdlib SigV4 (no AWS SDK). Live integration test on dev-linux-kvs against the deployed kdb-obj passes (round-trip, Range→206, delete). The first real Koder consumer is proven against the live substrate. Remaining for the row-18 flip: production cutover (depot main.go, owner-gated) + a 2nd consumer (Flow git, FLOW-148) → G5 production_proven.

kdb-obj deployed live — durable S3 object store on a DEV LXC (OSS-010 slice 2, 2026-06-08, /k-go)

The kdb-obj substrate went from proven-as-code to a running service. The kdb-obj-s3-server binary was rewired with engine selection (KDB_OBJ_ENGINE=ec → the durable ErasureEngine over a DiskShardStore across N failure-domain roots; kdb-obj-s3 now deps kdb-obj-ec) and deployed to a new LXC kdb-obj (10.0.1.101) on s.khost1: systemd kdb-obj-s3.service, EC engine 4+2 over 6 disk roots, bind :9300, dev SigV4 credential. Verified live (stdlib SigV4 client): 403 unsigned → createputget md5-match, head, range 206, survives rm -rf of a whole shard root (HTTP 200 + md5-match) and a service restart. The Koder-native S3 object store is now a live DEV endpoint. production_proven (row-18 gate) needs a soak window + real consumers (Hub depot / Flow git, OSS-011).

kdb-obj-store-kdb — kdb-table ShardStore for kdb-obj (OSS-010 slice 1, 2026-06-08, /k-go)

The durable object→shard map for kdb-obj, landing it on the kdb metadata plane (stack-RFC-006). New standalone crate crates/kdb-obj-store-kdb: KdbShardStore implements kdb_obj_ec::ShardStore over a kdb table reached via the pgwire (postgres-protocol) surface — the stable SQL interface, so it's insulated from kdb engine churn and works against real PostgreSQL too (the gold-standard test backend). Drop-in for DiskShardStore behind the same trait (no engine refactor); at deploy time it points at the kdb pgwire endpoint. Verified against real PG17 on dev-linux-kdb: the full ShardStore contract + the full ErasureEngine driven over the table (EC round-trip, DB-level shard drop → reconstruct, heal regenerates shards back into the table). Standalone workspace isolates the postgres dep from the engine lock. Remaining OSS-010 = Hub depot / Flow git consumers + the Koder ID CredentialStore (deploy-time integration).

auto-commit WAL is now default-on (789-ε / #543-2c, 2026-06-08, /k-go) — #789 COMPLETE

KDB_AUTOCOMMIT_WAL flipped to default-on: auto-commit writes are crash-recoverable + cross-region replicated by default (previously opt-in). This is the capstone of the #789 durable write-path unification (α extract kdb_commit · β group-commit fsync decouple · γ gateway WAL route · δ commit_lsn producer · ε this flip) and closes #543 slice 2c. Justified by #791 (fsync off the adapter lock) + #792 (off-lock group-commit, fsyncs 1984→385) amortizing the per-statement-fsync regression to 1.22× vs no-WAL (was 5.33×). auto_commit_wal_from_env split into a pure, env-race-free parse_auto_commit_wal (unit-tested: unset/junk → on, 0|false|off → off). Opt-out preserved per always-on.kmd R4.4; replica-transparent (WAL format unchanged, gate still needs a wal_writer). Full cargo test --workspace green (only the documented timing flake trips under parallel load). #543 remaining: only the multi-region soak (gated_by: evidence-soak).

kdb-obj-ec — healingscrubbing + Replicas(n): OSS-009 COMPLETE (OSS-009c, 2026-06-08, `k-go`)

Completeness of durability. Without scrubbing, slow shard loss eventually crosses the parity threshold → object lost; healing prevents that. kdb-obj-ec gained ErasureEngine::heal_object (regenerate missing shard slots from survivors — EC reconstruct+re-encode, or replica copy) + scrub(prefix) (the background sweep), plus a fully-wired DurabilityScheme::Replicas(n) mode (whole-object copies, cheaper than EC for small/hot objects). ObjectMetaRecord now carries a Scheme tag so objects self-describe. 22 tests green on dev-linux-kdb, incl. a proof that healing between two rounds of parity-count losses lets an object survive 4 total losses with parity 2. OSS-009 (the durable engine) is done — EC + disk failure-domain placement + healing + replication. Re-homed: the kdb-table object→shard map → OSS-010 (deploy-time, real kdb; the disk sidecar map is already durable); trust_tier-spread + throughput bench vs MinIO → OSS-011 (row-18 perf gate, inherently deploy-time).

kdb-wal — group-commit queue: fsync off the writer lock (kdb#792, 2026-06-08, /k-go)

The #791 decouple moved the fsync off the adapter lock but a 3-way bench (decoupled-WAL vs inline-WAL vs no-WAL, ext4 fsync ≈ 2.3 ms) showed auto-commit-WAL still cost 5.33× vs no-WAL — fsyncs did not coalesce (1984/2000) because GroupCommit::flush_through's leader held the WalWriter mutex during the fsync, blocking other committers' appends. #792 fixes it: WalWriter::clone_sync_handles dup()s the segment (+ timestamp-index) fd; the leader snapshots target + clones under the lock, releases it, fsyncs the clones off-lock, then bumps the counter. Concurrent committers append during the fsync and are made durable by the leader's single fsync (target taken under the lock → everything ..target is covered; durable_lsn never claims past it). Segment rolls fsync the closed segment under the lock, so the off-lock path only needs the live segment. Result: fsyncs 1984 → 385 (~5 commits/fsync under 32-way), auto-commit-WAL cost 5.33× → 1.22× vs no-WAL — conclusively unblocking 789-ε / #543-2c (flip KDB_AUTOCOMMIT_WAL default-on; replica-transparent, opt-out preserved). cargo test -p kdb-wal group_commit + cargo test -p kdb-pgwire (455) green.

kdb-obj-ec — durable DiskShardStore (failure-domain spread) + per-object EC (OSS-009b, 2026-06-08, /k-go)

The durable placement for kdb-obj's EC engine. src/disk.rs DiskShardStore spreads erasure shards across N disk roots = failure domains (shard j on a distinct root via a per-object base), replicates the meta blob to every root, and keeps all state on disk → restart-survivable and survives losing a whole disk (with num_roots ≥ data+parity, one dead disk = ≤1 lost shard/object). Drop-in for InMemoryShardStore via the ShardStore trait. put_with_policy now honours a per-object Ec{data,parity} scheme. 16 tests green on dev-linux-kdb incl. deleting a whole root dir then reconstructing via a fresh store. The object→shard map in a kdb table (stack-RFC-006 metadata plane) is deferred to a later ShardStore impl behind the same trait (needs a running kdb; kdb internals in flux under kdb#791) — the disk sidecar map is already durable. OSS-009c = healing/scrubbing + Replicas(n) + trust_tier spread + bench.

kdb-pgwire — auto-commit-WAL fsync decouple (kdb#791, 2026-06-08, /k-go)

Extended the #789-β group-commit fsync decouple to the auto-commit write-ahead path (the hot per-statement fsync, KDB_AUTOCOMMIT_WAL). The adapter write lock is owned upstream in engine_dispatch, not in run_with_adapter (which only borrows it), and run_with_adapter is also called nested by CALL/COPY with the guard already held — so the decouple is wired top-level-only via a new DeferredCommit out-param: execute_dml_with_triggers appends the WAL records (no fsync) under the held guard and parks the apply; engine_dispatch drops the guard, fsyncs off the lock, re-acquires, and applies. Gated to INSERT-only (writes_are_insert_only) + no-AFTER-trigger tables; nested callers pass None. Bench (ext4, fsync ≈ 2.3 ms): decoupled ~1.1–1.6× faster than inline — the win is the fsync moving off the adapter lock (committers pipeline), not fsync-count coalescing (unchanged). On tmpfs (fsync ≈ 2 µs) there is no win — never bench WAL on tmpfs. cargo test -p kdb-pgwire green (454 + new coalescing test). True cross-statement group-commit QUEUE + the no-WAL baseline → #792; the 789-ε / #543-2c default-flip stays its own deliverable.

kdb-obj-ec — durable erasure-coded engine landed (OSS-009a, 2026-06-08, /k-go)

The durability core of kdb-obj: a 25th workspace crate crates/kdb-obj-ec (reed-solomon-simd MIT/BSD + md-5 — no lock conflict with the kdb core). ErasureEngine implements ObjectEngine (erasure:true, durable:true): each object is split into data + parity Reed-Solomon shards placed via a ShardStore seam (InMemoryShardStore now; durable kdb-backed object→shard map = OSS-009b); reads reconstruct from whatever survives, tolerating up to parity_count lost shards — the durability the skeleton adapters (in-memory/local-FS) could not give. The canonical S3 ETag (hex MD5) is computed + stored here (moved from OSS-008b). Per ADR-001 this is the "compose a proven durability primitive" layer — Koder owns EC placement/orchestration on top of the proven reed-solomon-simd Galois math. 11 tests green on dev-linux-kdb incl. a chaos drop of parity_count-of-9 shards → full reconstruct. OSS-009 → in-progress; 009b = durable kdb ShardStore, 009c = healing/scrubbing + replication scheme + bench.

kdb-obj-s3 — S3 front now RUNNABLE: bucket lifecycle + hardening + server (OSS-008c, 2026-06-08, /k-go)

OSS-008 COMPLETE. kdb-obj-s3 gained src/bucket.rs (a BucketStore seam, per-tenant, mirrors CredentialStore; durable kdb impl = OSS-009) → bucket createheaddelete/list + NoSuchBucket on writes to unprovisioned buckets when a store is configured; a body-size guard (with_max_object_sizeEntityTooLarge, checked on declared length and streamed bytes — s3s ships none); and src/server.rs + a kdb-obj-s3-server binary (S3ServiceBuilder + KdbObjAuth + hyper-util) making the front a runnable S3 endpoint. Verified by 16 unit tests and a real HTTP smoke (curl: PUT bucket/obj→200, GET→body, Range: bytes=0-4→ 206 partial, missing→404) on dev-linux-kdb. The Koder ID CredentialStore impl moved to OSS-010 (consumer wiring); the durable engine is OSS-009.

kdb-obj-s3 — auth (access-key→tenant) + byte-range GET (OSS-008b, 2026-06-08, /k-go)

kdb-obj-s3 gained src/auth.rs: a CredentialStore trait (access key → secret + owning tenant), InMemoryCredentialStore, and KdbObjAuth implementing s3s::auth::S3Auth (default-deny). KdbObjS3::with_credentials + resolve_tenant bind a verified access key to its owning tenant, so a caller only reaches its own tenant's objects. Byte-range GET wired (Range::Int/Suffixengine.get_range, 206 + Content-Range). 10 tests green on dev-linux-kdb. S3-spec MD5 ETag re-scoped to OSS-009 (the metadata layer owns canonical ETags). Remaining 008c: hardening + bucket ops + the Koder ID CredentialStore impl + server wiring.

kdb-obj-s3 — S3 protocol front landed, dep-isolated workspace (OSS-008a, 2026-06-08, /k-go)

The S3 API front for kdb-obj: a new crate crates/kdb-obj-s3 implementing the s3s::S3 trait (Apache-2.0 s3s 0.13) over kdb_obj::ObjectEngine — core data plane (GetPutHeadDeleteListV2 + multipart). Per ADR-001 Koder owns the S3 surface; the engine owns durable bytes; s3s supplies only the protocol.

Dep-isolation (architectural): kdb-obj-s3 is its own workspace, NOT a kdb member — s3s 0.13 pins sha2 =0.11.0-rc.5, conflicting with the kdb core's stable sha2 0.11.0 (kdb-pgwire → tokio-postgres-rustls). Keeping the web-facing front's lock (s3shyperRC-crypto) out of the transactional core is correct (front = service, engine = library); it path-depends on ../kdb-obj. kdb_obj::ByteStream/ObjectStream widened to Send + Sync for StreamingBlob::wrap. 5 S3-front tests + kdb-obj 6/6 green on dev-linux-kdb. Auth (SigV4Koder ID) + range + RLS binding are OSS-008bc.

New crate kdb-obj — object-substrate seam landed (OSS-007, 2026-06-08, /k-go)

The kdb workspace gained a 24th crate, crates/kdb-obj — the object-storage member of the kdb family (stack-RFC-006; self-hosted-pairs row 18). Per ADR-001 kdb-obj is built by composing proven Rust primitives (s3s + reed-solomon-simd + kdb metadata), not by wrapping a monolithic S3 server. OSS-007 ships the swappable ObjectEngine trait boundary (object_store-shaped core + Koder's PlacementPolicy vocabulary) + two skeleton adapters (InMemoryEngine, LocalFsEngine) + a reusable conformance suite (incl. cross-tenant isolation), 6 tests green on dev-linux-kdb. The durable engine (reed-solomon EC + kdb shard metadata) is OSS-009; the s3s S3 front + Koder ID auth is OSS-008. Substrate is object-native (no WALMVCCRaft on the bytes) — it shares the kdb brand/workspace, not the transactional substrate.

HTTP-SQL gateway writes now WAL-streamed cross-region — RFC-008 gap CLOSED (#789-γ, 2026-06-08, /k-go kdb)

The POST /v1/sql HTTP-SQL gateway was a production write path (Koder ID users/orgs, the AI gateway audit/stream, DNS platform, migration scripts) whose writes bypassed the WAL — accepted on a primary, applied straight to the substrate, never streamed cross-region. Under RFC-008 (WAL-streaming is THE cross-region mechanism) a tenant written via HTTP-SQL could not have a working secondary region. #789-0 added a telemetry counter for exactly these writes; #789-γ closes the gap: the gateway DML arm now routes through the same shared kdb_commit durable-commit pipeline pgwire uses. On a primary with a WAL writer, an HTTP-SQL write captures into a statement-local buffer (AdapterWithSeqs::capturing, deferred apply), WAL-appends + fsyncs before the substrate apply (write-ahead), then 2PC-applies — so it now carries a replica-replayable BeginInsertCommit and is cross-region streamable. The pgwire AdapterWithSeqs was promoted to pub (the reused capture seam); SqlState gained the WAL writer + CommitCtx counters, wired in main.rs to the SAME WAL head pgwire + the WalStreamServer share. Dev mode / no-writer / replica keep the byte-identical execute_dml path. Verified on dev-linux-kdb: new gateway test proves the WAL carries the gateway INSERT (replica-replayable); kdb-gateway 321+1, kdb-commit+kdb-pgwire lib 404/0, pgwire_v2 454/0. Regression #1066.

#789-δ (same session) — cross-protocol RYW producer: SqlResponse gained commit_lsn: Option<u64> (skip_serializing_if → wire-additive, absent for readsno-writerreplica). A WAL'd gateway write surfaces its WAL commit LSN; a client echoes it as the next request's min_lsn (the read-floor consumer was already shipped, #543 slice 3-followup) → full cross-protocol read-your-writes (write via HTTP-SQL, read forwarded only to a replica caught up past that write). The γ test asserts the INSERT response carries Some(lsn>0) and a SELECT carries None; kdb-gateway 322/0. Regression #1072. #789: γ + δ DONE — only ε remains (flip #543 2c, gated on #791 + a bench).

Group-commit fsync decouple SHIPPED for INSERT-only commits — #543 2b-ii partial via #789-β (2026-06-08, /k-go kdb)

The 2b-ii wiring landed for the provably-safe subset, refining the earlier "needs apply-ordering" claim (below): that requirement holds for concurrent same-row / ReplaceAll writes, but INSERT-only commits decouple safely with no apply-ordering mechanism. INSERT WAL records are self-contained (carry their full row_data) and inserts commute — a WAL replica replaying in LSN order converges regardless of the primary's apply order — so the fsync can leave the substrate lock without divergence. An INSERT-only explicit-txn COMMIT now: append WAL records under the adapter lock → release the lock → GroupCommit::flush_through (coalesced fsync, off the lock so concurrent committers share one fsync) → re-acquire → 2PC apply. Write-ahead preserved (fsync precedes apply). ReplaceAll/Tombstone keep the inline-fsync path. Gated default-on by KDB_GROUP_COMMIT (kill-switch).

Two boundaries the investigation hardened, now their own tickets: #790 — the ReplaceAll WAL Delete records diff append-time physical state (guard.scan), so decoupling lets a concurrent committer make that diff stale → divergence even with ordered apply; the fix is apply-order-independent (real-PK / logical) UPDATE/DELETE records (which also closes the long-standing recover_from_wal Delete-replay gap). #791 — the auto-commit write-ahead path (the hot per-statement fsync that gates the 2c flip) holds the adapter lock upstream in run_with_adapter, so its decouple needs a lock restructure. New kdb_commit API: writes_are_insert_only (safety predicate), wal_append_writes (append, no fsync — shared append_commit_records helper keeps the inline path byte-identical), wal_flush_through (coalesced fsync, direct-sync() fallback), CommitCtx.group_commit. Verified on dev-linux-kdb: kdb-commit 2/2, cargo test -p kdb-pgwire 453 passed (new decoupled round-trip test + the pre-existing WAL test now exercises the path). Regression #1061. 2b is now PARTIAL (INSERT-only via β); the ReplaceAll half (#790) + auto-commit hot path (#791) remain, and convergence to caminho a (the ordered WalApplier, below) supersedes both once it lands.

#543 slice 2b-ii re-scoped — "primary adopts the ordered WalApplier" (caminho a stage 2); building blocks all exist (2026-06-08, /k-go kdb, design only)

The crash-no-gap gate of the group-commit wiring was investigated and it rules out wiring the fsync into the existing inline-apply path. Two facts: recover_from_wal is additive-only (re-inserts committed Inserts, skips Deletes, never truncates the substrate to the WAL frontier), and commit_kv_tx is independently durable (TiKV/Raft). So c-wal-ssot (apply before fsync) diverges — a crash leaves a durable substrate write absent from the WAL that recovery never removes → the primary holds a write no replica sees. And c-strict (fsync before apply) with the adapter lock released around the fsync breaks WAL-order=pply-order on concurrent same-row writes → primary/replica divergence. Effective group commit therefore needs an apply-ordering mechanism, not just the fsync coordinator — i.e. the ordered WalApplier (#776). 2b-ii ≡ the primary applies its own WAL through that applier = caminho a stage 2. De-risking find: all the blocks already existGroupCommit (2b-i), WalApplier, impl ReplicaSink for RecordAdapter (the primary's own commit type is already a valid sink), and the applier's applied_lsn watermark is the #543 RYW floor. Sharp-edge audit (next /k-go): the secondary-index edge is CLOSEDMutableRowSource::append for RecordAdapter (the applier's flush path) already maintains vectortrigramdoc-GIN + BTree, so an applier-driven primary apply and recover_from_wal both stay index-correct. Open: faithful Delete/Update replay — the WAL Delete carries a format!("{row:?}") debug key (not a real PK), so recover_from_wal skips deletes today (a latent recovery gap); the #410 real-PK wire-format fix is 2b-ii's first sub-slice. And MVCC-tag survival + local visibility via applied_lsn need characterization before the rewire. Full audit in the #543 ticket.

Group-commit fsync coordinator primitive (2026-06-08, /k-arch/k-go kdb #543 slice 2b-i)

The /k-arch resolved the slice-2b fork → caminho a (log-structured unified apply), staged, with stage-1 = c (move the fsync out of the global adapter write lock via a group-fsync coordinator, preserving write-ahead). New kdb_wal::GroupCommit is the load-bearing mechanism: a leader/follower coordinator coalescing WalWriter::sync across concurrent committers — flush_through(commit_lsn) returns once an fsync covering that LSN completed; the first arrival leads (one fsync), the rest wait and return covered with no redundant fsync, so fsync_count ≤ commit_count (≪ under load). It holds the writer lock (brief) during the fsync, not the caller's adapter lock — the decoupling that the wiring step delivers. Built + proven in isolation (deterministic coalescing + durability + 16-thread tests, red-verified; kdb-wal lib 144/144) before the hot-path wiring, which carries a cross-durability-domain ordering gate (WAL file vs KV-substrate durability) that must not be rushed. Remaining: 2b-ii wires it into commit_txn + the auto-commit branch with the crash-no-gap gate (the fsync must not be acked-durable behind the substrate apply); 2c flips KDB_AUTOCOMMIT_WAL default-on once 2b-ii lands; convergence to a moves the inline apply onto the unified WalApplier (#776).

2-region RYW read-property tests + slice 2b group-commit re-scoped to a D12 fork (2026-06-08, /k-go kdb #543 slice 5)

Slice 5 (correctness half)kdb-gateway's read-forward hook gains property tests over a matrix of replica applied_index (what the WAL apply loop advances in prod) × read floor, on the real GatewayReadForwardHook + route_read + ReplicaRegistry: the RYW safety invariant (forward to a replica iff applied_index >= floor), the liveness catch-up (behind-floor → primary, then served by the replica once it applies past the floor), a genuine two-region route-around (affinity-local replica behind the floor + caught-up peer elsewhere → served by the peer — the floor filter spans the whole healthy set), and the tenant-tier gate (an EventuallyConsistent tenant ignores the floor). Red-verified (neutering decide_with_floor's floor filter fails the 3 floor-dependent tests); kdb-gateway lib 320320 (was 316). The read-latencyreplica-lag half is an evidence-soak on real multi-region hardware.

Slice 2b re-scoped (D12, no code) — the planned "group commit amortizes the per-statement fsync" is a no-op today: both commit paths (auto-commit DML and explicit-txn commit_txn) fsync the WAL while holding the process-global RwLock<RecordAdapter> write lock, so commits are fully serialized before reaching the WAL — there are no concurrent committers to coalesce. The real root cause is the fsync held under that global write lock; the structurally-correct fix decouples the WAL flush latch from the adapter write lock while preserving write-ahead ordering (else the rejected option-B WAL-behind-KV divergence window returns) — an owner-relevant durabilityreplication restructure routed to a dedicated `k-arch`. 2c (flip auto-commit WAL default-on) stays gated on that decision, not merely on "group commit".

RYW read-floor wiring into the cross-region read seam + per-tenant gate (2026-06-08, /k-go kdb #543 slice 3)

Cross-region read-forwarding now honours read-your-writes: a connection's own last write (its WAL commit LSN, captured in ConnActivityEntry.last_write_lsn) is threaded into the read-forward router so a non-Strong read is never served by a replica that hasn't yet applied that write. The floor-aware router (RouteRead::decide_with_floor, which filters replicas by applied_index >= floor and falls back to the primary with ReplicaBehindReadFloor) already existed since slice 1 — but both read-forward call sites passed a hardcoded read_floor = None, so the captured floor never reached routing. Slice 3 wires it: execute_select_pipeline resolves the floor (read_floor_for_conn) and passes it across the ReadForwardHook seam (trait gained a read_floor arg).

Gated per tenant: GatewayReadForwardHook carries the bound tenant's ReplicaConsistency (resolved at boot from SubstrateTopologyCatalog) and honours the floor only for ReadYourWrites; the default EventuallyConsistent tier (RFC-008 §6.1) ignores it and keeps reading any in-tolerance replica — so wiring the floor is a zero-behaviour-change for the common tenant. The separate gRPC/HTTP-SQL read path (sql_service.rs, #774) has no per-session activity entry and stays on the eventual path pending a request-scoped floor (e.g. a min_lsn header). Red-verified pgwire wiring test + 3 gateway tenant-gate tests; kdb-pgwire 452452 + kdb-gateway lib 316316 green. Steps 134 of the write half (slice 2a) and the read floor (slice 3) are in; 2b/2c (group-commit + flip) and 5 (2-region property test) remain.

Auto-commit write-ahead WAL path, gated default-off (2026-06-08, /k-go kdb #543 slice-2a)

The auto-commit write path (the common write shape — a bare INSERTUPDATEDELETE with no explicit BEGIN) can now be routed through the same durable, replicated write-ahead pipeline the explicit-transaction COMMIT path uses: wal_precommit_writes (WAL append + fsync) runs before the 2PC KV apply. Until now auto-commit writes persisted via the inline KV-only path and emitted no WAL → they were invisible to WAL replay-recovery and to cross-region edge replicas (which apply WAL, RFC-011). This is the behavioral core of the #543 caminho-a write-path unification.

Gated default-off behind PgwireState::auto_commit_wal (env KDB_AUTOCOMMIT_WAL): write-ahead means one fsync per statement (~100× the inline write latency) until group-commit (slice 2b) amortizes it, so the default flip (slice 2c) is perf-benchmarked + owner-visible. The branch reuses apply_writes_2pc_with_seqs with the caller-held sequences guard (the seqs-decoupling refactor, step 2, exists precisely to avoid re-locking state.sequences → deadlock) so the #770 index-mirror co-population and the slice-1 secondary-index maintenance run on this path too; per-statement writes go into a statement-local buffer (never the shared __auto_commit__ sentinel, so concurrent connections don't collide). Two regression tests in pgwire_v2.rs (one red-verified gate-on, one default-off guard); kdb-pgwire lib 404404 + pgwire_v2 451451 green. Steps 134 done; 2b/2c (group-commit + flip), 3 (read-seam floor + tenant gate), 5 (2-region property test) remain.

COPY rows invisible to PK index — orphaned fix recovered (2026-06-08, /k-go kdb #788/#770)

Rows loaded via COPY FROM were invisible to PRIMARY-KEY / BTree index seeks (WHERE pk = v, WHERE pk >= v returned nothing) while sequential scans found them. The COPY flush wrote the bare RecordAdapter instead of wrapping it in AdapterWithSeqs, so the in-memory index mirror (state.sequences) that Plan::IndexSeek reads was never co-populated — unlike every other write path.

This was originally fixed under #770 (2026-06-03) but the commit was orphaned by a concurrent-commit race and never landed on master; the transactional half had been independently restored by #787 (slice-1 of the #543 write-path unification), and #788 recovered the COPY half. On the current tree both COPY flush sites were bare — the streaming on_copy_data site (added since June 3 with the #500/#507 COPY_FLUSH_ROWS=10_000 batching) and the final on_copy_done site — so both now route through AdapterWithSeqs (lock order adapter → sequences, matching engine_dispatch). Two regressions in sql_dump_restore.rs cover both sites (3-row → on_copy_done; 12_000-row → streaming on_copy_data), both red-verified. The separate #771 (gin_trgm_ops LIKE returns empty) was already shipped on master via a CREATE-INDEX backfill, so it was not re-filed.

Bottomless WAL: real S3 archive backend (2026-06-04, /k-go kdb #717)

The S3Archive WAL-archive backend went from skeleton (NotImplemented) to a working object_store-backed implementation — kdb can now archive sealed WAL segments to S3-compatible object storage (the self-hosted MinIO at kdrive), the cold tier behind PITR and the cold source-of-truth for cross-region edge replicas (#543).

Client decision (/k-arch 2026-06-04): object_store (Apache Arrow) over hand-rolled SigV4-over-reqwest and rust-s3 blocking. It's a backend-agnostic seam (forward-compatible with the native kdb-obj object plane, stack-RFC-006, which itself targets feature_parity:s3_api), reuses the workspace reqwest+rustls (no second HTTP/TLS stack), and avoids owning crypto-adjacent signing code forever (D-maintainability / Quality>Speed). self-hosted-first is satisfied — the service is the Koder-hosted MinIO; the client crate is unencumbered (no Koder candidate exists yet).

Impl (crates/kdb-wal/src/archive.rs): S3Archive { inner: Result<S3Inner, String> } keeps new() infallible (the archive_from_config factory requires it) by capturing a build failure and surfacing it as ArchiveError::Backend at first use — never a panic. The sync WalArchiveBackend trait bridges to the async object_store API via an owned current-thread runtime (block_on; callers are the sync uploader / kdbctl wal restore, never inside a Tokio runtime). Path-style + allow_http for MinIO; canonical key {tenant}/wal/{lo:016x}-{hi:016x}.seg shared with FsArchive; tenant isolation by key prefix; NotFound mapped from object_store::Error::NotFound for restore gap-detection. Verde on dev-linux-kdb against a hermetic MinIO: tests/s3_archive_minio.rs (env-gated, CI-safe skip) 4 scenarios + kdb-wal 141/141 + kdb-cli builds. Regression #885.

Bottomless WAL: BlobArchive (Koder Drive) backend (2026-06-06, /k-go kdb #773)

The last stubbed archive backend (BlobArchive, Koder Drive) went from NotImplemented to a real object_store backend — every [wal.archive] backend (nonefss3/blob) is now functional. Decision (Regra 13): Koder Drive exposes an S3-compatible face (infra/data/blob/src/s3compat.rs, SigV4), so blob reuses the proven object_store wire (no 2nd HTTP/TLS stack) pointed at Drive's S3-compat endpoint — not a thin alias over s3 (which targets the MinIO on kdrive → would duplicate s3), and not a bespoke client against the still- design-phase native kdb-blob HTTP API. Distinct from s3 via Drive's bucket = tenant tenancy: BlobArchive builds an AmazonS3 client per tenant (bucket = tenant_id, lazily cached behind a Mutex), key wal/{lo:016x}-{hi:016x}.seg (no tenant key-prefix — the bucket is the tenant), so cross-tenant reads are impossible by construction (separate buckets), stronger than S3Archive's key-prefix isolation. SigV4 creds from the single token_env parsed as access:secret (keeps the {url, token_env} config shape). Same infallible-new() + Backend-at-first- use pattern as S3Archive. Verde on dev-linux-kdb: kdb-wal lib 141/141 + blob_archive_drive 44 (builderror contract: unset/malformed token + unreachable endpoint all → typed Backend, never panic) + s3_archive_minio 1/1. Regression

895. Gated integration test (KDB_BLOB_TEST_ENDPOINT) CI-safe-skips — Koder Drive

is design-phase, no live harness yet.

doc-GIN + vector index backfill on CREATE INDEX (2026-06-04, /k-go kdb #772)

Symmetry follow-up to #771: register_document_gin_index (#537) and register_vector_index (#540) had the identical forward-only gap — they wired the per-append maintenance hook but never built the index over rows that already existed at CREATE INDEX time. doc-GIN: the read seam routes document_gin_lookup(...).or_else(scan), so empty postings return Ok(empty) and collapse to [] (no scan fallback). vector: a registered index does not fall back to brute-force, so an empty HNSW graph returned the wrong nearest neighbours.

Fix (mirrors #771): backfill_document_gin_index (scan existing rows → extract_jsonb_ops_terms per evaluate_jsonpath(path)SubstrateDocumentGinPostings::add_terms) and backfill_vector_index (scan → insert each vector into the HNSW handle by pk_to_vector_id). Gotcha: the record layer has no Vector ColumnType — a SQL VECTOR(n) column is stored as Bytes (packed LE f32), so a row decoded via raw_to_values carries Value::Bytes, not Value::Vector (which only the write-path Row the append hook sees has); the vector backfill reverses the packing. Both backfills live in register_* (the CREATE path), never in insert_*_handle (shared with rehydrate_secondary_indexes), so a restart never double-inserts — structurally, not by idempotency luck. Verde on dev-linux-kdb: pgwire_document_gin_scan 1919, pgwire_vector_search 22, kdb-adapter --lib 1111, kdb-adapter --test integration 3737 (all *_rehydrates_across_restart + replace_all), trigram suites unaffected. Regression #883.

gin_trgm_ops trigram index backfill on CREATE INDEX (2026-06-04, /k-go kdb #771)

Bug (spawned by #770): CREATE INDEX … USING gin (col gin_trgm_ops) over a table that already had rows built empty postings, so a LIKE/~ that routed to a TrigramScan returned [] instead of the matching rows.

Root cause: RecordAdapter::register_trigram_index (crates/kdb-adapter/src/lib.rs) only registered the in-memory handle + persisted metadata and relied on the forward sync_trigram_indexes_after_append hook to fill postings — it never built the index over rows that existed at CREATE INDEX time. PostgreSQL builds an index over current data; the path was forward-only. TrigramScan (trigram_lookup(...).or_else(scan)) falls back to a seq scan only on Err, not on Ok(empty), so the empty superset collapsed the result. The passing sibling test creates the index before inserting, masking it.

Fix: new backfill_trigram_index(&ct, col, fold_case) invoked inside register_trigram_index — scans existing rows (scan_all_raw_stamped), extract_trigrams for the target column, adds postings via SubstrateTrigramIndex::add (idempotent; posting key embeds row_pk). Targets only the indexed column so it never double-counts a sibling index. Verde on dev-linux-kdb: pgwire_trigram_opclass_ddl 5/5 (+ new guard gin_trgm_ops_backfill_then_incremental_both_visible), pgwire_trigram_routing 66, kdb-adapter --lib 1111. Regression #882. Spawn #772: doc-GIN (#537) and vector (#540) registrations have the identical forward-only gap.

kdbctl build fix + vector migration codec (2026-06-03, /k-go kdb #768)

O binário kdbctl (kdb-cli) não buildava no master desde a #759 fatia 4: main.rs importava model_migrate_sink::{KdbNextRecordSink, KdbNextVectorSink} sem pub mod model_migrate_sink; no lib.rs, e o sink referenciava um codec de vetor (encode/decode_vector_payload, decode_vector_pk, vector_pk) nunca landado. Corrigido:

  • pub mod model_migrate_sink; exportado.
  • Codec de linha-de-vetor migrada em model_migrate.rs (contrato que o

    adapter Go-kdb #758 vai emitir): pk = vector_id BE 8-byte; payload = dim ‖ f32[dim] ‖ metadata length-prefixed LE, metadata ordenada por chave (verify checksum-estável — o gateway devolve KV em ordem arbitrária). Auto-contido, sem prost (payload é interno à migração, não contrato cross-service).

  • Verde: cargo build -p kdb-cli (bin OK), lib 18/18 (+6 codec), e

    migrate_vector_grpc 3/3 (gateway Vector real in-process sobre sled temp) — a acceptance da #759 que nunca foi build-verificável agora passa.

  • Spawn #770 — bug pré-existente de escaping COPY FROM (tab/backslash não

    sobrevive round-trip), descoberto ao tornar cargo test -p kdb-cli rodável.

TLS metadata in pg_stat_ssl + vendored pgwire patch (2026-06-03, /k-go kdb #711)

Fechou §2 (sslversioncipherbits) e §3 (clientdnclientserialissuerdn mTLS) do pg_stat_ssl, que estavam parkados desde 2026-05-09 por um bug do pgwire 0.38.3: ClientInfo::client_certificates fazia downcast do stream embrulhado no enum privado MaybeTls e panicava em toda conexão TLS. Confirmado ainda presente no 0.39.0 e 0.40.0 — esperar upstream não era viável.

  • Fork vendorizado em infra/data/kdb/third_party/pgwire/ (0.38.3 +

    patch shape-B), aplicado via [patch.crates-io] no workspace root. O patch captura peer_certificatesprotocol-versioncipher no negotiate_tls (espelhando a captura de SNI já existente) e os expõe por client_certificates() / tls_protocol_version() / tls_cipher() sem tocar no stream privado. Temporáriothird_party/pgwire/PATCH.md tem o delta + checklist de remoção pra quando o upstream landar o fix. Primeiro [patch.crates-io] do monorepo kdb.

  • Normalização rustls→PG em kdb-pgwire/src/handler/tls_meta.rs

    (TLSv1_3TLSv1.3, TLS13_…TLS_…, bits AES/ChaCha).

  • e2e reais: tls_e2e_pg_stat_ssl_reports_version_and_cipher (§2) e

    tls_e2e_mtls_populates_client_dn (§3, CA + client-cert via rcgen).

  • Verde: kdb-pgwire lib 404/404, pgwire_tls_e2e 7/7, kdb-gateway

    build OK. Também corrigida regressão pré-existente pg_depend (asserção stale do #351 vs. row do #352d).

  • §1 kTLS kernel offload desmembrado pro #769 (independente; exige mudar

    o tipo de MaybeTls no fork + gate de bench 15% num host kTLS-capable).

Multi-region Phase 8.1 — tenant primary_region + kdbctl region (2026-06-02, /k-go kdb #761, slice 1)

Primeira fatia buildável da campanha multi-region (RFC-008, anchor #757). Entrega a fundação de metadados de região que #762 (read-routing) e #763 (failover) consomem:

  • record: CatalogTenant.primary_region: Option<String> + CatalogTenantWire

    tag 3 (prost-optional → linhas pré-#761 decodificam como None, backward-compat). Catalog::set_tenant_primary_region (CAS, idempotente, NotFound em tenant inexistente, região vazia rejeitada) + lookup_tenant_record. rename_tenant e o backfill de tiers preservam a região.

  • proto/gateway: primary_region em LookupTenantResponse + RPC novo

    Catalog.AssignTenantRegion.

  • kdb-cli: kdbctl region show <tenant> + `kdbctl region assign tenant

    region | --clear (via CatalogClientHandle::{lookuptenantregion, assigntenantregion}`).

Metadados apenas — não move dados nem re-elege nó (Phase 8.4). Testes: record 580580 (+6), gateway lib 217217 (+2). #761 segue pending — faltam region list (precisa ListTenants), promote/demote (wire no active-active do gateway, não flip de metadado), heartbeat e mTLS.

Multi-region Phase 8.3 — read routing + cross-region read-forward (2026-06-04, /k-go kdb #762 + #774, SHIPPED)

Phase 8.3 do RFC-008 completa: leituras eventuais são roteadas para uma réplica da região local, leituras strong ficam no primário.

  • Controle de consistência (#762): read_consistency (`strong | snapshot |

    stale-NNN) — GUC de sessão no **pgwire** (nome **bare** read_consistency, não kdb.) + campo por-request read_consistency no read path **HTTP-SQL** (#774). Threada até RecordAdapter::scan_consistency` → TiKV stale-read (follower) / leader-read. Ausente no HTTP → caminho committed legado, byte-idêntico.

  • Transporte cross-region (#774): o primário encaminha uma leitura não-Strong

    para uma réplica da região preferida via o serviço gRPC ReadForward (route_read + GrpcReadForwarder cliente + ReadForwardService/ GatewayReadExecutor servidor). RouteRead::decide_with_affinity escolhe a réplica (afinidade de região, filtro de staleness); erro de transporte / sem réplica elegível → fall-back-to-local (disponibilidade > localidade). O ConsistencyAdapter (wrapper RowSource) é o análogo gateway-side do hook de consistency do pgwire. Réplica registra o ReadForwardServer no listener gRPC; o primário semeia o ReplicaRegistry via --read-forward-replica id=…,addr=…,region=…,lag_ms=… (seam estática; o heartbeat da Phase 8.2 alimenta o mesmo registry depois). preferred_region = primary_region do tenant (#761) → role.region.

  • Verificação: teste e2e in-process (cross_region_read_forward_full_path) +

    sim real de 2-regiões (dois processos kdb-gateway region-tagged sobre gRPC real, dev-linux-kdb): stale-500 no primário → route_path=Forwarded → linha da réplica us-ia; strong → linha local do primário. gateway lib 224 verde.

  • Pending (#775, low): forwarding no read path do pgwire; auth

    service-to-service (SVID L2) no RPC ReadForward; pass tc netem em 2 LXCs (release build) pro contraste de RTT 80 ms-vs-<5 ms.

Multi-region — replica-side WAL apply: the missing foundation (2026-06-04, /k-go kdb #776, SHIPPED)

Antes do #776 o data-plane de replicação não fechava end-to-end: havia WAL writer (pgwire), stream server (WalStream.Subscribe) e stream client (kdbctl wal backup, só PITR), mas nada aplicava o WAL transmitido numa réplica. O read-routing do #774 encaminhava leituras eventuais para réplicas que, em prod, estariam vazias/stale (a sim do #774 semeava a réplica à mão). O #776 entrega a parede que faltava:

  • WalApplier (kdb-gateway/src/wal_applier.rs): replay txn-atômico

    (buffer por txn_id, aplica no Commit, descarta no Abort), idempotente por watermark de LSN de commit, num MutableRowSource. Replaya o Row do planner pelo mesmo write-path do primário (append / delete_rows por chave-debug) — não put_raw_stamped: o Insert.row_key/Delete.pk do WAL são strings format!("{row:?}"), não chaves de storage (mesma semântica que o recover_from_wal).

  • Subscribe-and-apply loop (replica_apply_loop.rs): a réplica disca o

    WalStream.Subscribe do primário, alimenta o applier, dá Ack do watermark, resume do último LSN aplicado no reconnect. Compartilha o mesmo Arc<Mutex<RecordAdapter>> do ReadForward → leituras encaminhadas enxergam os writes replicados. Heartbeat-push (run_replica_heartbeat_push) reporta a posição aplicada viva pro ReplicaRegistry do primário (substitui a seam estática --read-forward-replica).

  • Binário (main.rs): WalStreamServer montado no primário (gate --wal-dir,

    head do WalWriter compartilhado com o commit pgwire) + ReplicaSyncServer (mesmo ReplicaRegistry do read-routing). Numa réplica (--role replica --primary-addr --sql-tenant): spawna o apply loop + heartbeat.

  • Verificação: 14 testes unit/integração verdes (wal_applier 7,

    replica_apply_loop 2 contra o servidor gRPC WalStream real in-process, replica_sync_service 5) + e2e real de 2 processos (scripts/776-replication-e2e-smoke.sh, dev-linux-kdb): BEGIN; INSERT×3; COMMIT no primário → apply loop applied_txns=1 → réplica retorna as 3 linhas em ~0.5s (< gate 1s p99), replicado via WAL apply, sem hand-seed. Fecha o gap de prod do #774 + RFC-008 Phase 8.2; destrava #763 + #543.

  • Fronteiras descobertas (ticketadas, não regressões): DDL não é replicado via

    WAL (só DML) → schema da réplica precisa de pre-seed (#777, high). Writes auto-commit não escrevem WAL (handler/state.rs) → INSERT tem que estar em BEGIN; … COMMIT. Smoke shell → teste cargo gated (#778, SHIPPED 2026-06-05).

Multi-region — committed cargo regression gate p/ o e2e de replicação (2026-06-05, /k-go kdb #778, SHIPPED)

Promove o smoke shell scripts/776-replication-e2e-smoke.sh (rodado à mão) a um teste cargo gated (crates/kdb-gateway/tests/replication_e2e.rs, gate KDB_GATEWAY_REPL_E2E=1, no slot pesado de CI como o D2 smoke) — o wiring cross-process (flags do binário → WalStreamServer → subscribe-apply loop → read da réplica) agora é regression gate (policies/regression-tests.kmd #894). Spawna primário+réplica em portas :0 (OS-picked), CREATE TABLE + BEGIN;INSERT;COMMIT no primário, polla /v1/sql da réplica até as 3 linhas replicarem puramente via WAL apply — réplica sem pre-seed (o #777 já replica o DDL, então a tabela é auto-criada do WAL antes da DML). Converge no 1º poll (300ms) em dev-linux-kdb.

Infra reutilizável (além do ticket): os drills cross-process hardcodavam portas gRPC/HTTP fixas (frágeis sob CI paralelo). Adicionados 2 ready-signals de produção espelhando o KDB_PGWIRE_LISTENING: KDB_GRPC_LISTENING (o serve gRPC migrou de serve_with_shutdown p/ pre-bind + serve_with_incoming_shutdown, então a porta gRPC OS-picked é descobrível p/ --primary-addr) + KDB_HTTP_LISTENING, com helpers genéricos wait_for_listening_ready/wait_for_listening_markers em kdb-jepsen::faults::kill. Destravam a remoção de portas fixas nos drills #763/

781#782 (follow-up). Não-quebrador: D2 gated 33 + kdb-jepsen faults lib 21/21

seguem verdes.

Multi-region — DDLschema replication over WAL (2026-06-04, `k-go kdb` #777, SHIPPED)

Fecha a fronteira do #777: o WAL passa a carregar DDL além de DML, então uma réplica recém-provisionada cria sozinha a tabela das linhas replicadas — sem o pre-seed de schema que o #776 exigia. Design estruturado (não raw-SQL): record WalRecord::Ddl { op: DdlOp, table, payload } com payload bincode opaco (Schema), emitido no sucesso de create_table_adapter.rs (gated wal_writer && !standby), aplicado na réplica via um supertrait ReplicaSink: MutableRowSource (RecordAdapter::create_table/drop_table/replace_schema/rename_table) — o apply loop e o main.rs não precisaram de fiação nova (o adapter já fluía). Bug achado+corrigido (latente também no #776): o Ddl é o 1º record do WAL (lsn 0); o gate de watermark lsn <= applied_lsn com watermark inicial 0 dava 0 <= 0 e PULAVA o DDL → corrigido com sentinela u64::MAX ("nada aplicado"). Verificação: 10 testes unit/integração verdes + binário builda + e2e real de 2 processos (scripts/777-ddl-replication-e2e-smoke.sh): réplica de kv VAZIO auto-cria a tabela do DDL + recebe as 3 linhas em ~0.5s, sem pre-seed. PASS. Vertical CREATE TABLE; DROPALTERRENAME emit pendente (#779, o applier já trata os 4 DdlOp).

#779 (SHIPPED, 2026-06-04): DROPALTERRENAME TABLE agora emitem WalRecord::Ddl via um helper compartilhado emit_ddl em handler/statements/mod.rs (CREATE refatorado pra usá-lo). e2e (scripts/779-ddl-drop-alter-rename-e2e-smoke.sh): primário faz CREATE+INSERT+ ALTER ADD COLUMN+RENAME+DROP → réplica de kv vazio aplica tudo em ordem, SELECT k,v,w FROM kv2 retorna as 3 linhas com a coluna nova. PASS. Fronteira achada: a réplica roda dois RecordAdapter (apply-loop + http SQL) com caches de catálogo separados → re-leitura de um nome cacheado antes de um DDL fica stale (nomes novos = read-through ok); fix estrutural = compartilhar um adapter/catálogo na réplica (#780).

#780 (SHIPPED, 2026-06-04): a réplica passa a usar um único RecordAdapter/catálogo compartilhado entre apply-loop + ReadForward + HTTP SQL (via SqlState::with_shared_adapter, mudança de 1 bloco no main.rs). Um DDL replicado invalida o cache único → leituras HTTP nunca veem schema/existência stale de um segundo adapter. Verificado (scripts/780-replica-shared-adapter-verify.sh): cacheia kv na réplica → DROP no primário → re-leitura na réplica reporta table not found no 1º poll (antes ficava stale).

Multi-region — WAL fan-out async-pull driver (2026-06-06, /k-go kdb #543, slice 1)

O fan-out cross-region tinha os tipos de topologia (fatia 1), o catalog substrate (fatia 4), o componente de decisão de roteamento (fatia 2) e o apply-loop single- replica do #776 — mas faltava o driver que liga topologia → subscrição WAL real por edge region. Adicionado WalFanoutSupervisor (crates/kdb-gateway/src/wal_fanout_supervisor.rs): o driver async-PULL da reconciliação RFC-008 §4 (reconciliada 2026-06-04 — não um push síncrono no commit path). Num gateway edge (seu --region, fatia 3b), pra cada topologia que lista esta região em edge_regions, spawna um run_replica_apply_loop (reuse-first, máquina do #776 verbatim) subscrevendo o home region do tenant, resolvido via um novo ReplicaEndpointMap (region→addr; a topologia carrega nomes, não endereços). Home sem endpoint → skip+warn (fault-isolation: um misconfig não silencia outros tenants). FanoutConsumer::applied_lsn() expõe o watermark por-consumer (sentinel u64::MAX=nada-aplicado) — o que a slice RYW vai consumir. Opt-in, zero blast-radius (componente lib novo; sem wiring no caminho defaultcommitreadTLS). Verde dev-linux-kdb (--lib wal_fanout_supervisor 55: selection, fault-isolation, e2e pull-apply contra WalStream gRPC in-process, endpoint-map, RYW default). Regression #896. #543 segue pending: restam RYW LSN-gated reads (estende #774), wiring no main.rs atrás de flag, e sim 3-region (aceite).

Multi-region Phase 8.4 — automatic failover CORE (2026-06-04, /k-go kdb #763, CORE SHIPPED)

O mecanismo central de failover automático está fiado + verificado por chaos drill. O PromotionWatchdog (já existente, testado) foi ligado na réplica: o heartbeat driver alimenta o relógio de ack; se o primário fica silencioso além de --failover-timeout-secs, o watchdog dispara run_promotion. Hooks de produção (promotion.rs): GatewayRoleFlip (limpa is_standby do pgwire → o nó promovido aceita escritas + para os loops follower), NoopPd (sled/sim; hook pd-ctl real = #782), InMemoryPromotionLock, e o driver async run_promotion_watchdog. Chaos drill (scripts/763-failover-chaos-drill.sh): primário CREATE TABLE (schema replica via #777) → kill -9 no primário → a réplica promove; escrita pgwire rejeitada (25006 standby) antes vira aceita depois. RTO = 3268 ms ≪ gate 30 s; RPO sub-segundo (apply loop #776 mantém lag <1 s). 10 testes unit verdes. Pendente (split-out): #781 (HIGH) = quorum + lock fenced/compartilhado — o lock atual é per-process, então com >1 réplica duas poderiam promover → split-brain (gate de correção antes de prod multi-réplica; o core #763 só é seguro com 1 réplica). #782 (medium) = kdbctl region failoverrollback + automação de DNSLB + hook PD real.

#781 fail-safe guard (SHIPPED, 2026-06-04 — footgun fechado, ticket segue aberto): o watchdog de auto-failover não arrisca mais split-brain silenciosamente. Default agora é seguro: só auto-promove com --failover-single-node (atesta réplica única) — pois o InMemoryPromotionLock é per-process (sem exclusão mútua cross-réplica). Verificado (scripts/781-failover-failsafe-verify.sh): sem o flag → primário morto → réplica fica read-only (watchdog recusa, logado); com o flag → promove. O consenso real (lock fenced/ quorum) segue aberto em #781 — na arquitetura kdb-next/TiKV o vencedor-único autoritativo é PD/Raft (entrelaçado com o hook PD do #782); não forjar no sled sim.

Blocker pré-existente descoberto (#768, high): o binário kdbctl não compila em master desde o #759 fatia 4model_migrate_sink.rs é referenciado por main.rs mas nunca exportado em lib.rs, e importa encode/decode_vector_payload/decode_vector_pk que não existem em model_migrate.rs (vector codec do #759 nunca landou). Finalizar o #759 não é escopo do #761 (design do dono). O código de região do #761 type-checka limpo por cima disso (build reporta só o 1 erro pré-existente); smoke do kdbctl region aguarda #768. Lib do kdb-cli + record + gateway compilam e testam verde.

Online additive secondary-index evolution — Catalog.AddIndex (2026-06-02, /k-go kdb #765)

Antes, adicionar um índice secundário a uma tabela já populada era impossível sem recriar a tabela (perda de dados / migração à mão): Catalog::ensure_table falha fechado (schemas_matchConflict "different schema") quando o schema ganha um índice novo, e os consumidores (Koder Hub depot, hub#105) caíam no fallback "log + opera no schema legado", deixando o índice novo como dead code.

Entregue o caminho de evolução aditiva online:

  • Catalog::add_indexes(tenant, &desired_schema) (record layer,

    kdb-record/src/catalog.rs) — classifica o diff stored→desired via classify_index_diff: Identical → no-op idempotente; Additive (colunas base + PK inalteradas, todo índice existente byte-for-byte igual, ≥1 índice novo) → registra as adições via replace_table_schema; Destructive (qualquer mudança de colunaPK, ou índice existente removidorenomeado/unique- flipado) → Conflict, e o chamador mantém o fallback legado. Nunca aplica destrutivo silenciosamente — esse caminho continua sendo o framework explícito migrate.

  • gRPC kdb.v1.catalog.Catalog/AddIndex — recebe o Schema desejado completo

    (mesma forma do EnsureTable) + expected_fingerprint (guarda de staleness); retorna created, added_indexes, fingerprint, client_backfill_required. Colocado no Catalog, não no Record service esboçado no ticket (Regra 13): evolução de schema pertence ao catálogo (name-based, fingerprint-guarded, irmão do EnsureTable); o Record service raw nem segura um handle de catálogo.

  • Split de backfill — o record layer é row-shape-agnostic, então não deriva

    componentes de índice de payloads opacos. Sobre a superfície raw-gRPC (id, hub) o backfill é client-driven (o chamador re-PutIndexed cada linha após o registro); tabelas tipadas (SQL adapter) mantêm o backfill server-side resumível migrate::add_index (cursor na migration-state row durável).

  • Métrica kdb_index_backfill_progress{tenant,table,index} no GatewayMetrics.

Desbloqueia hub#105 (by_version_platform_arch unique-upsert no catálogo de 20 apps já existente). 8 testes novos (7 record-layer de diffidempotênciarejeição + 1 gateway end-to-end: create→100 linhas→AddIndex→backfill cliente→100 lookups, idempotência, rejeição destrutiva). Validado em dev-linux-kdb: record 574/574, gateway lib 215215, obs 88.

Achado lateral → resolvido (#767, mesma sessão): o teste de integração crates/kdb-gateway/tests/wire_compat.rs não compilava — o match independente sobre kdb_record::ColumnType não cobria BlobRef. Investigado: o caminho de produção já estava completo (o proto kdb.v1.schema já declara COLUMN_TYPE_BLOB_REF = 15 e o convert.rs já mapeia nos dois sentidos), então BlobRef é confirmadamente wire-visible (column type real do #542, discriminant 15, 24 bytes, parte do fingerprint). Faltava só o braço do match do teste (guard de drift deliberado — quebrou a compilação corretamente quando o enum cresceu). Fix: braço BlobRef => gen::ColumnType::BlobRef + coluna BlobRef no sample_schema() do teste (cobre o encoding do discriminant 15 de fato). cargo test -p kdb-gateway (todos os targets) verde no dev-linux-kdb.

kdb backlog sweep — graph SQL functions, kdb-tx analysis, pg_dump split (2026-05-11, /k-go kdb)

Triagem dos 3 tickets pendentes do backlog (infra/data/kdb/backlog/pending/):

  • #316 (kdb-tx extract) — fechado como won't-do após auditoria. Premissa do ticket estava errada: módulos tx/mvcc/lock/deadlock não existem em kdb-record/src/ (apenas audit, branch, catalog, cow_kv, error, fingerprint_history, keys, merge, migrate, schema, tenant, tier, wire). A tx API já vive em kdb-kv-trait (KvTx trait + IsolationLevel); MVCClocksdeadlock são delegados aos backends (sled local, TiKV Percolator 2PC); RecordTx é wrapper fino sobre Box<dyn KvTx>. Tickets #163-#167 shipados na kv-traitbackends, não em kdb-record. RFC-001 §Roadmap não lista kdb-tx nas sub-RFCs pendentes. Criar crate vazio de re-exports violaria hyperscale-first sem benefício. Análise completa em `backlogdone/316-kdb-tx-extract-crate.kmd`.
  • #326 (graph SQL functions) — shipado. 4 algoritmos do kdb-graph agora expostos como table-valued functions SQL: pagerank('graphs', 1), connected_components('graphs', 1), louvain('graphs', 1), shortest_path_weighted('graphs', 1, from, to). Arquitetura segue o padrão Plan::VectorSearch (#062): novo plan node Plan::GraphCall { function, args, output_columns }, novo trait method RowSource::graph_function(name, args), builder build_graph_call em build.rs, impl RecordAdapter::graph_function em kdb-adapter/src/lib.rs que abre kdb_graph::Graph::new(kv, tenant, table_id) via block_in_place + rt.block_on. Divergência consciente do contrato proposto: arg table TEXT adicional à esquerda do graph_id porque kdb-graph e kdb-record compartilham o namespace (tenant, table_id) sem registry SQL-level de "qual record-table backs qual graph". Quando shipar CREATE GRAPH (futuro), overload sem table arg fica óbvio. Test roundtrip em kdb-adapter/tests/integration.rs::test_graph_sql_functions_via_adapter valida shape de coluna, cardinalidade e invariantes do algoritmo (PageRank sum1.0, CC2 componentes, shortest-path resolve 1→2→3 na cycle + zero rows no caso desconectado). Pgcatalog match sites atualizados: `outputnames, foldplan, substouterplan, bindparamsinplan, planhasouterrefs, nodelabel/jsonnodetype/children (explain), collectseqscantables/findtableinplan/findlockrowsclause` (pgwire planwalk). 17/17 adapter tests verdes; workspace inteiro cargo build --workspace verde. Follow-ups: round-trip via psql socket (gateway integ tooling, ticket futuro) e otimizer pass pra GraphCall (perf gate; #327+).
  • #333 (pg_dump v17 compat) — fechado como split. Escopo grande demais pra prioridade baixa num único PR; quebrado em 4 sub-tickets independentes em backlog/pending/:
    • #339 — Audit do query stream do pg_dump v17 contra Postgres real (PG17 docker + tcpdump + diff report vs kdb-gateway v0.0.13). Spec normativa em meta/docs/stack/specs/postgres-compat/pg-dump-v17.kmd (a criar). Bloqueia #341 e #342.
    • #340 — Wire-protocol COPY <table> TO STDOUT (CopyOutResponse + CopyData text + CopyDone). Hoje só COPY ... TO '<file>' existe; comment em copy_file.rs:248 referencia pgwire_traits.rs que não existe (stale).
    • #341 — pgcatalog coverage (pgattribute v17 rows, pgsequence, pggetserialsequence, pggetfunctionarguments, pgconstraint, pg_type) — depende de #339.
    • #342 — Round-trip test harness pg_dump | pg_restore contra Postgres vanilla v17 — depende de #339 + #340 + #341. CI gate weekly + on-demand.

#260 (que marcou pg-dump-restore-compat done prematuramente) fica efetivamente reaberto pelo split.

Estado atual do backlog kdb (post-sweep): 0 pending dos 3 originais; 4 pending novos (#339-#342) com dependências explicitamente declaradas.

Graph perf runbook — infra/data/kdb/docs/runbooks/graph-pagerank-perf.kmd (2026-05-10, /k-go kdb #329)

Runbook humano operacional ata o tripé #318 (algoritmos) + #327 (bench) + #328 (CI gate). 7 seções: como rodar o bench, como ler output, como interpretar baseline, como atualizar baseline, como diagnosticar regressão (4-step ladder: phase isolation, git bisect com comando pronto, flamegraph, CI artifact diff), quando triggerar full gate, follow-ups conhecidos. Path canônico via policies/content-location.kmd.

Graph perf CI gate — .gitea/workflows/kdb-graph-perf-gate.yml (2026-05-10, /k-go kdb #328)

Workflow novo gateando regressão de perf do PageRank em CI. 2 jobs:

  • quick — 100k1M, gate snapshot+pr ≤ 5s (baseline 1.380s). PushPR.
  • full — 1M/10M, gate snapshot+pr ≤ 60s (RFC-012 promise, baseline 25.062s). workflow_dispatch apenas.

Baseline em infra/data/kdb/crates/kdb-graph/perf/baseline.json (4 escalas + thresholds + notes). Parser bash extrai walltime do stderr do bench; awk faz comparison sem dep de bc. Follow-up #329: runbook humano em docs/performance/graph-pagerank.kmd.

Graph perf validation — RFC-012 promise empiricamente confirmada (2026-05-10, /k-go kdb #327)

kdb-graph/examples/pagerank_scale.rs shipou. cargo run --release --example pagerank_scale -p kdb-graph mede end-to-end. RFC-012 promise (1M nodes / 10M edges < 60s) validado: 25.062s no laptop do dev (sled local). Scaling linear; PageRank é 78.5% do walltime, snapshot 21.5%. Follow-up #328 plug em CI nightly.

Graph v2 — PageRank + Louvain + connected components + Dijkstra (2026-05-10, /k-go kdb #318)

kdb-graph cumpre a promessa do README v0.8.0 §"Graph" (PageRank + community detection + shortest path). 4 algoritmos novos em algorithms.rs:

  • Graph::pagerank — power iteration com dangling-node mass redistribution. Convergence L1, final renorm.
  • Graph::connected_components — union-find na projeção undirected, ordering determinístico.
  • Graph::louvain — greedy modularity (single-level), tie-break por community id.
  • Graph::shortest_path_weighted — Dijkstra min-heap, rejeita pesos negativos.
  • Graph::snapshot — primitive público pra reuso entre algoritmos no mesmo grafo (1 substrate scan).

37 testes verdes em kdb-graph (eram 23). Pesos lidos de ("weight", "<float>") prop; ausente ⇒ 1.0 (unweighted). Follow-ups: #326 (SQL function exposure), #327 (bench em escala 1M/10M).

Observability — RFC-002 §6§7§4 fechados (2026-05-10, /k-go kdb)

5 tickets fechados num único lote: #321 (backlog status/location policy), #312 (OTLP metrics exporter Rust), #313 (OTel logs wiring Rust), #315 (semantic conventions §4), #046 (umbrella close — lado Rust 100%).

  • kdb-obs::metrics::OtelInstruments + GatewayMetrics::new_with_otel(meter_provider) — Prometheus → OTLP mirror. Nomes RFC-002 §6.1 (kdb.query.duration, kdb.rpc.count, etc.) emitidos via OTel; aliases Prometheus (kdb_*_seconds) preservados pra compat de dashboards.
  • kdb-gateway::main ganha 6 CLI flags --otel-* que controlam o pipeline. Quando enabled, chama init_tracing(OtelConfig) (já existia em kdb-obs::tracing_setup) e passa meter_provider pro GatewayMetrics. Init failure é non-fatal (fallback fmt-only).
  • Semantic conventions §4 plugadas em 8 spans da SQL chain: db.kdb.model = "relational" cross-stack; db.kdb.tenant_id nos spans pgwire (do_query simple/extended); placeholders pra db.kdb.rows_returned, db.kdb.cache_hit, db.kdb.plan_hash (recordáveis pelos call sites).
  • policies/backlog.kmd § Status ↔ location consistency — regra nova + script meta/context/scripts/audit-backlog-status-location.sh. kdb cleanup: 119 mismatches → 0. Stack-wide drift (2637) tracked em projects/koder-stack#125.

Lado Go (#314) fica como track separado pós-Phase 7.4. 10 testes em kdb-obs (eram 7), 357 em kdb-pgwire (sem regressão).

Multi-tenancy — #322 lote 2 done (2026-05-10, /k-go kdb)

FoundationID v2 bearer JWT chega na surface pgwire. Fecha o ticket #322 — lote 1 já tinha estabelecido koder_user_idworkspace_id no AuthContext e o helper Tenant::from_user_workspace; lote 2 plugou tudo no StartupMessage.

  • PgwireState.jwt_validator: Option<Arc<JwtValidator>> — campo novo (espelha metrics do #310). Single canonical Arc<JwtValidator> por processo, partilhado com os 6 interceptors gRPC do #320 — JWKS cache amortizada cross-surface.
  • KdbScramStartupHandler + KdbStartupHandler — short-circuit FoundationID v2 antes do challenge SCRAMcleartext: cliente passa koder_id_token=<jwt> no StartupMessage, validador checa, finish_authentication fecha o handshake. Sem token ⇒ caminho SCRAM legado 100% inalterado. Token inválido ⇒ FATAL/SQLSTATE 28000 antes do banner.
  • Cross-tenant gate — quando state.tenant_id != 0 (listener bound a um tenant kdb específico), o tenant canônico derivado do token tem que casar; senão rejeita com status=cross_tenant. Cumpre specs/multi-tenancy/contract.kmd § error model "404 not 403".
  • canonical_tenant_id(&AuthContext) -> u64 — ponto de adoção do Tenant::from_user_workspace: usa o helper quando koder_user_id está presente, fall-back pro claims.tid legado v1.
  • Audit log dedicado (tracing::info!(target: "kdb.audit.jwt", …)) — toda validação JWT emite linha estruturada (timestamp, user_id, tenant_id, token_jti, status, source_ip). 8 status estáveis pra parsing.

Tests: 30 verdes em auth_handler (eram 14) — 8 round-trip com JWKS server stub (T1–T8 do specs/identity/login-resolution-test-template.kmd + cross-tenant T9 do contract) + 8 unit tests pros helpers (canonical, AuthError mapping, validate edge cases). 26 em kdb-auth sem regressão.

Multi-tenancy — #322 lote 1 (2026-05-10, /k-go kdb)

Foundations da identidade canônica do policies/multi-tenant-by-default.kmd.

  • AuthContext ganha koder_user_id + workspace_id (OptionString).
  • JwtClaims deserializa os campos novos com #[serde(default)] (compat com Foundation/ID v1).
  • HmacValidator popula os campos novos como None.
  • Tenant::from_user_workspace(user_id, workspace_id) — single canonical mapping (BLAKE3-truncated, domain-separation byte, catalog-namespaced, nunca colide com TENANT_SYSTEM).

26 kdb-auth + 6 novos tenant tests verdes. Lote 2 fechado na mesma janela (entrada anterior).

Security wave — #320 done (2026-05-10, /k-go kdb)

Foundation/ID JWKS integration no gateway gRPC.

  • kdb-gateway::auth ganha modos with_jwt(secret, jwt) e

    jwt_only(jwt) além do new(secret) legacy. Token detection por shape (looks_like_jwt).

  • kdb-gateway::main ganha CLI flags --jwt-jwks-url /

    --jwt-issuer / --jwt-refresh-secs. Arc<JwtValidator> único reusado pelos 6 interceptors gRPC.

  • O crate kdb-auth (Phase 2.4) já tinha JWT/JWKS validation completa

    — o gap real era apenas wiring. 24 tests passando.

  • #322 novo (prioridade alta · seg, filho de #320): pgwire bearer

    JWT no StartupMessage + claim-mapping koder_user_id/workspace_id pra atender policies/multi-tenant-by-default.kmd + specs/multi-tenancy/contract.kmd.

Observability evolve wave — lote 2 (2026-05-09, /k-go kdb)

Mesma sessão estendeu o #311 para cobrir a SQL chain inteira:

  • kdb-sql::parse_sql ganha span kdb.parse (RFC-002 §5.1).
  • kdb-planner::build + build_with_catalog_and_params ganham span kdb.plan.
  • kdb-planner::exec::{execute, execute_with_params, execute_dml} ganham span kdb.execute.
  • kdb-pgwire Simple + Extended do_query ganham span root kdb.query com surface = "pgwire.simple"|"pgwire.extended".

O pipeline RFC-002 §5.1 está agora coberto ponta-a-ponta: kdb.querykdb.parsekdb.plankdb.executekdb.storage.read|write. #315 (adoção das semantic conventions §4 nos spans existentes) está agora desbloqueado.

Tests verdes: 90 kdb-sql + 340 kdb-pgwire + 653/655 kdb-planner (2 falhas pré-existentes documentadas abaixo em v0.0.13).

Observability evolve wave (2026-05-09, /k-evolve)

First lote do gap observability levantado pela RFC-002 (OpenTelemetry):

  • #310 donekdb-gateway agora expõe /metrics em port 9301 quando

    --metrics-bind (ou KDB_METRICS_BIND) é setado. GatewayMetrics é construído como Arc e compartilhado com PgwireState, então pgwire query histograms passam a aparecer no scrape Prometheus do gateway.

  • #311 done (lote 1)#[tracing::instrument] aplicado em

    Record::{put,get,delete,scan}_stamped, Catalog::{ensure,lookup}_{tenant,table} e nos handlers gRPC Record.{Put,Get,Delete,Scan}. Spans seguem RFC-002 §5.1 (kdb.querykdb.storage.read|write, kdb.catalog.*) e carregam os attributes §4 (db.system, db.operation, db.kdb.tenant_id, table_id).

  • *046 voltou pra pending