Berserk Docs

Meta gRPC API

Catalog management, ingestion, merge tasks, and cluster operations

The meta service exposes a gRPC API on port 9560 (configurable via META_BIND). It manages all cluster metadata: databases, tables, views, ingestion streams, merge tasks, and segment lifecycle. gRPC reflection is enabled for tooling support.

Services Overview

ServiceDescription
DatabaseServiceManages top-level databases (the unit that owns tables and views).
TableServiceManages tables within a database, including sharding fields, vector fields, retention, and merge-span configuration.
ViewServiceManages named, database-scoped unions over tables. A view name lowers to the same plan shape as an explicit union T1, T2, ... in KQL.
ClusterServiceCluster membership registry — meta tracks live cluster members for leader-election and routing.
IngestionServiceAuthenticates ingest tokens and manages collector streams.
IngestTokenServiceManages ingest tokens for authentication and signal routing.
NurseryServiceManages ingest stream lifecycle, merged segment registration, and offset tracking.
MergeTaskServiceCoordinates segment merge tasks between the janitor and merge workers.
SegmentDeletionServiceManages cleanup of tombstoned segments from storage.
SegmentLookupServiceDiscovers segments matching a time range and table filter.
JanitorServiceProvides cluster statistics and merge task management for maintenance operations.
VidxServiceThe vector-index write gate and the per-format segment inventory.

DatabaseService

Manages top-level databases (the unit that owns tables and views).

RPCs

RPCDescription
CreateDatabaseCreate a new database
ListDatabasesList all databases
DeleteDatabaseTombstone a database. Without cascade, rejects when any live table / view / ingest token still lives in the database. With cascade, tombstones every live catalog row in the database in one transaction; rows already tombstoned keep their own tombstone and expire no later than the database.
ResurrectDatabaseLift the tombstone on a database within its grace window, and on the children its cascade tombstoned. Children deleted on their own before the database stay tombstoned.
PreviewDeleteDatabaseLive children a cascade DeleteDatabase would tombstone. Read-only and advisory (children may change before the delete).
SetDatabaseDescriptionAbsent or blank description clears it. Trimmed; at most 2000 characters.

Proto Definition

service DatabaseService {
  rpc CreateDatabase(CreateDatabaseRequest) returns (CreateDatabaseResponse);
  rpc ListDatabases(ListDatabasesRequest) returns (ListDatabasesResponse);
  // Without cascade, rejects if any live child (table/view/token) remains; tombstoned children do not block.
  rpc DeleteDatabase(DeleteDatabaseRequest) returns (DeleteDatabaseResponse);
  rpc ResurrectDatabase(ResurrectDatabaseRequest) returns (ResurrectDatabaseResponse);
  // Live children a cascade DeleteDatabase would tombstone. Read-only and advisory (children may change before the delete).
  rpc PreviewDeleteDatabase(PreviewDeleteDatabaseRequest) returns (PreviewDeleteDatabaseResponse);
  // Absent or blank `description` clears it. Trimmed; at most 2000 characters.
  rpc SetDatabaseDescription(SetDatabaseDescriptionRequest) returns (SetDatabaseDescriptionResponse);
}

message Database {
  string id = 1;
  string name = 2;
  // Absent for live rows.
  Tombstone tombstone = 3;
  // Free-text operator description; absent when none is set.
  optional string description = 4;
}

message CreateDatabaseRequest {
  string name = 1;
}

message CreateDatabaseResponse {
  Database database = 1;
}

message ListDatabasesRequest {
  bool include_deleted = 1;
}

message ListDatabasesResponse {
  repeated Database items = 1;
}

message DeleteDatabaseRequest {
  string database_id = 1;
  bool cascade = 2;
  // Grace window zero; bytes still bound by the segment tombstone_min_age floor (~12 min).
  bool immediate = 3;
}

message DeleteDatabaseResponse {
  Database database = 1;
}

message ResurrectDatabaseRequest {
  string database_id = 1;
}

message ResurrectDatabaseResponse {
  Database database = 1;
}

message PreviewDeleteDatabaseRequest {
  string database_id = 1;
}

// One live child (id + display name) a cascade delete would tombstone.
message DeletableChild {
  string id = 1;
  string name = 2;
}

message PreviewDeleteDatabaseResponse {
  repeated DeletableChild tables = 1;
  repeated DeletableChild views = 2;
  repeated DeletableChild ingest_tokens = 3;
  // Grace window (seconds) a non-immediate delete schedules before the
  // database is permanently removed — meta's effective value, so the UI
  // need not hardcode it.
  int64 grace_window_seconds = 4;
}

message SetDatabaseDescriptionRequest {
  string database_id = 1;
  optional string description = 2;
}

message SetDatabaseDescriptionResponse {
  Database database = 1;
}

TableService

Manages tables within a database, including sharding fields, vector fields, retention, and merge-span configuration.

RPCs

RPCDescription
ListTablesList tables in a database
CreateTableCreate a new table
GetTableGet table by ID
DeleteTableTombstone a table. Rejects when live views or live ingest tokens still reference the table, returning a structured inventory of the blockers.
ResurrectTableLift the tombstone on a table within the grace window.
SetShardingFieldsConfigure table sharding fields
GetShardingFieldsGet table sharding configuration
ResetShardingFieldsRestore the default sharding fields a new table is created with
SetVectorFieldsReplace the table's vector-search fields (at most 4 property keys; an empty set clears them)
GetVectorFieldsGet the table's vector-search fields
SetTableRetentionSet per-table retention (unset clears the override)
SetTableMaxMergeTimeSpanSet the per-table merge-span cap (unset reverts to retention-derived default)
SetTableDescriptionTable Description. Absent or blank description clears it. Trimmed; at most 2000 characters.

Proto Definition

// All RPCs are database-scoped via an explicit `database_id`. Writes
// keyed on `(database_id, name)` to match the table's per-database
// uniqueness; id-keyed reads use `(database_id, table_id)` and the
// server verifies membership.
service TableService {
  rpc ListTables(ListTablesRequest) returns (ListTablesResponse);

  // Table CRUD
  rpc CreateTable(CreateTableRequest) returns (CreateTableResponse);
  rpc GetTable(GetTableRequest) returns (GetTableResponse);
  // Rejects with a TableReferences inventory when any live view or ingest token still references the table.
  rpc DeleteTable(DeleteTableRequest) returns (DeleteTableResponse);
  rpc ResurrectTable(ResurrectTableRequest) returns (ResurrectTableResponse);

  // Table Sharding Fields
  rpc SetShardingFields(SetShardingFieldsRequest) returns (SetShardingFieldsResponse);
  rpc GetShardingFields(GetShardingFieldsRequest) returns (GetShardingFieldsResponse);
  // Restores the shard set every new table is created with.
  rpc ResetShardingFields(ResetShardingFieldsRequest) returns (ResetShardingFieldsResponse);

  // Table Vector Fields. Replaces the set; at most 4 property keys, each at most
  // 256 bytes once canonicalised. An empty set means none.
  rpc SetVectorFields(SetVectorFieldsRequest) returns (SetVectorFieldsResponse);
  rpc GetVectorFields(GetVectorFieldsRequest) returns (GetVectorFieldsResponse);

  // Table Retention & Merge Span
  rpc SetTableRetention(SetTableRetentionRequest) returns (SetTableRetentionResponse);
  rpc SetTableMaxMergeTimeSpan(SetTableMaxMergeTimeSpanRequest) returns (SetTableMaxMergeTimeSpanResponse);

  // Table Description. Absent or blank `description` clears it. Trimmed; at most 2000 characters.
  rpc SetTableDescription(SetTableDescriptionRequest) returns (SetTableDescriptionResponse);
}

// region: Common Types

enum BqlType {
  BQL_TYPE_BOOL = 0;
  BQL_TYPE_INT = 1;
  BQL_TYPE_LONG = 2;
  BQL_TYPE_REAL = 3;
  BQL_TYPE_STRING = 4;
  BQL_TYPE_DATETIME = 5;
  BQL_TYPE_TIMESPAN = 6;
  BQL_TYPE_GUID = 7;
  BQL_TYPE_DYNAMIC = 8;
}

message Column {
  string name = 1;
  BqlType type = 2;
}

// endregion

// region: Table Messages

message CreateTableRequest {
  string database_id = 1;
  string name = 2;
}

message CreateTableResponse {
  string id = 1;
  string name = 2;
  string database_id = 3;
}

message GetTableRequest {
  string database_id = 1;
  string table_id = 2;
  bool include_deleted = 3;
}

message GetTableResponse {
  string id = 1;
  string name = 2;
  string database_id = 3;
  // Retention in nanoseconds. Unset = no retention configured.
  optional int64 retention_ns = 4;
  // Explicit per-table merge span cap in nanoseconds. Unset = derive from
  // retention (or fall back to the global cap).
  optional int64 max_merge_time_span_ns = 5;
  // Absent for live rows.
  Tombstone tombstone = 6;
  // Free-text operator description; absent when none is set.
  optional string description = 7;
}

message ListTablesRequest {
  string database_id = 1;
  bool include_deleted = 2;
}

message ListTablesResponse {
  repeated Table items = 1;
  // Grace window (seconds) a non-immediate table delete schedules — meta's
  // effective TABLE_GRACE, so the UI need not hardcode it.
  int64 grace_window_seconds = 2;
}

message Table {
  string id = 1;
  string name = 2;
  string database_id = 3;
  optional int64 retention_ns = 4;
  optional int64 max_merge_time_span_ns = 5;
  // Absent for live rows.
  Tombstone tombstone = 6;
  // Free-text operator description; absent when none is set.
  optional string description = 7;
  // Empty when none are configured.
  repeated VectorField vector_fields = 8;
}

message DeleteTableRequest {
  string database_id = 1;
  string table_id = 2;
  // Grace window zero; bytes still bound by the segment tombstone_min_age floor (~12 min).
  bool immediate = 3;
}

message DeleteTableResponse {
  oneof outcome {
    Table tombstoned = 1;
    // Live views/tokens still referencing the table; tombstoned references are omitted (they do not block).
    TableReferences blocked = 2;
  }
}

message TableReferences {
  repeated ViewReference views = 1;
  repeated IngestTokenReference ingest_tokens = 2;
}

message ResurrectTableRequest {
  string database_id = 1;
  string table_id = 2;
}

message ResurrectTableResponse {
  Table table = 1;
}

message ViewReference {
  string id = 1;
  string name = 2;
  // Other tables this view spans; excludes the target table.
  repeated TableRef other_members = 3;
  // Absent for live rows.
  Tombstone tombstone = 4;
}

message IngestTokenReference {
  string id = 1;
  string name = 2;
  // Signal columns ("traces"/"logs"/"metrics") of this token pointing at the target table.
  repeated string signals = 3;
  // Absent for live rows.
  Tombstone tombstone = 4;
}

message TableRef {
  string id = 1;
  string name = 2;
}

message SetShardingFieldsRequest {
  string database_id = 1;
  string table_id = 2;
  repeated ShardingField fields = 3;
}

// The set as stored, with each name canonicalised.
message SetShardingFieldsResponse {
  repeated ShardingField fields = 1;
}

message ResetShardingFieldsRequest {
  string database_id = 1;
  string table_id = 2;
}

// The default set, as stored.
message ResetShardingFieldsResponse {
  repeated ShardingField fields = 1;
}

message GetShardingFieldsRequest {
  string database_id = 1;
  string table_id = 2;
  bool include_deleted = 3;
}

message GetShardingFieldsResponse {
  repeated ShardingField fields = 1;
}

message SetVectorFieldsRequest {
  string database_id = 1;
  string table_id = 2;
  repeated VectorField fields = 3;
}

// The set as stored, with each name canonicalised.
message SetVectorFieldsResponse {
  repeated VectorField fields = 1;
}

message GetVectorFieldsRequest {
  string database_id = 1;
  string table_id = 2;
  bool include_deleted = 3;
}

message GetVectorFieldsResponse {
  repeated VectorField fields = 1;
}

// Set per-table retention. Unset `retention_ns` clears the setting
// (reverts to "no retention configured").
message SetTableRetentionRequest {
  string database_id = 1;
  string table_id = 2;
  optional int64 retention_ns = 3;
}

message SetTableRetentionResponse {
  optional int64 retention_ns = 1;
}

// Set the per-table merge-span override. Unset `max_merge_time_span_ns`
// clears the override (planner falls back to the retention formula).
message SetTableMaxMergeTimeSpanRequest {
  string database_id = 1;
  string table_id = 2;
  optional int64 max_merge_time_span_ns = 3;
}

message SetTableMaxMergeTimeSpanResponse {
  optional int64 max_merge_time_span_ns = 1;
}

message SetTableDescriptionRequest {
  string database_id = 1;
  string table_id = 2;
  optional string description = 3;
}

message SetTableDescriptionResponse {
  Table table = 1;
}

// endregion

ViewService

Manages named, database-scoped unions over tables. A view name lowers to the same plan shape as an explicit union T1, T2, ... in KQL.

RPCs

RPCDescription
CreateViewCreate a new view
GetViewGet view by ID
ListViewsList views in a database
SetViewTablesAtomic full replacement of a view's member tables
DeleteViewTombstone a view. Views always use the 5-minute SHORT_GRACE (no immediate flag).
ResurrectViewLift the tombstone on a view within the grace window.
LookupTableOrViewDatabase-scoped name resolution returning either a table or a view (used by the binder)

Proto Definition

// ViewService manages named, database-scoped unions over tables. A view is
// "this database has a thing called `MyView` that means: union over
// T1, T2, T3" — the engine binder lowers a view-named reference into the
// same plan shape as an explicit `union T1, T2, T3` in KQL.
//
// All RPCs are database-scoped via an explicit `database_id`. Cross-type
// name collisions (a table and a view sharing a name in the same
// database) are rejected by CreateView / CreateTable at the service
// layer. No Delete RPC in v1.
service ViewService {
  rpc CreateView(CreateViewRequest) returns (CreateViewResponse);
  rpc GetView(GetViewRequest) returns (GetViewResponse);
  rpc ListViews(ListViewsRequest) returns (ListViewsResponse);
  // Atomic full replacement of the view's member list. Server rejects
  // an empty `table_ids` (a view with no members has no defined
  // schema and can't be lowered to a plan).
  rpc SetViewTables(SetViewTablesRequest) returns (SetViewTablesResponse);
  // Short default grace (5 min): views hold no recoverable data, so the window is only fat-finger insurance.
  rpc DeleteView(DeleteViewRequest) returns (DeleteViewResponse);
  rpc ResurrectView(ResurrectViewRequest) returns (ResurrectViewResponse);
  // Database-scoped name resolution for the engine binder. Returns
  // either a Table or a View. Replaces the unscoped
  // `list_tables() + find_by_name` fallback that lived in the binder
  // before PR 4b. Cross-type collisions are blocked at write time, so
  // a name resolves to exactly one variant.
  rpc LookupTableOrView(LookupTableOrViewRequest) returns (LookupTableOrViewResponse);
}

message View {
  string id = 1;
  string name = 2;
  optional string description = 3;
  // Ordered list of member tables (by `ordinal`). The order is
  // stable across `SetViewTables` and surfaces directly in the plan.
  repeated ViewMember members = 4;
  // Absent for live rows.
  Tombstone tombstone = 5;
}

// One member of a view's union. Carries both id and name so the
// binder can resolve without a second round-trip.
message ViewMember {
  string id = 1;
  string name = 2;
  // The member table's vector fields; empty when none are configured.
  repeated VectorField vector_fields = 3;
}

message CreateViewRequest {
  string database_id = 1;
  string name = 2;
  // Ordered list of member table UUIDs. Must be non-empty.
  repeated string table_ids = 3;
  optional string description = 4;
}

message CreateViewResponse {
  View view = 1;
}

message GetViewRequest {
  // Database that owns the view. Required. Server rejects with
  // INVALID_ARGUMENT if `database_id` does not own `view_id`.
  string database_id = 2;
  string view_id = 1;
  bool include_deleted = 3;
}

message GetViewResponse {
  View view = 1;
}

message ListViewsRequest {
  string database_id = 1;
  bool include_deleted = 2;
}

message ListViewsResponse {
  repeated View items = 1;
}

message SetViewTablesRequest {
  // Database that owns the view. Required. Server rejects with
  // INVALID_ARGUMENT if `database_id` does not own `view_id`.
  string database_id = 3;
  string view_id = 1;
  // Atomic full replacement. Must be non-empty.
  repeated string table_ids = 2;
}

message SetViewTablesResponse {
  View view = 1;
}

message DeleteViewRequest {
  string database_id = 1;
  string view_id = 2;
}

message DeleteViewResponse {
  View view = 1;
}

message ResurrectViewRequest {
  string database_id = 1;
  string view_id = 2;
}

message ResurrectViewResponse {
  View view = 1;
}

message LookupTableOrViewRequest {
  // UUID of the database to resolve `name` against. Callers are
  // expected to have already resolved via `GetDatabase`.
  string database_id = 1;
  string name = 2;
  bool include_deleted = 3;
}

// Result of a `LookupTableOrView`: the name resolves to exactly one of
// a Table or a View. Caller can match on the `kind` oneof to dispatch.
message LookupTableOrViewResponse {
  oneof kind {
    LookupTable table = 1;
    LookupView view = 2;
  }
}

message LookupTable {
  string id = 1;
  string name = 2;
  // Empty when none are configured.
  repeated VectorField vector_fields = 3;
}

message LookupView {
  string id = 1;
  string name = 2;
  repeated ViewMember members = 3;
}

ClusterService

Cluster membership registry — meta tracks live cluster members for leader-election and routing.

RPCs

RPCDescription
RegisterMemberRegister the calling node and refresh its heartbeat
ListMembersList currently-registered cluster members
DeregisterMemberRemove a registration. Best-effort, called on graceful shutdown to close the up-to-one-TTL window in which a departed member still looks alive. Idempotent: removing an absent slot succeeds with removed=false. Metas that predate this RPC answer UNIMPLEMENTED; callers must ignore that.
ListVidxLevelsEvery live replica that opens segments, with the vector-index format level it last reported: nurseries from their poll (or, while they have no fresh poll, their service registration), query workers from their lane registration, and janitors and query front ends (which may read segments in process) from their service registration.
ListGatewayKeysThe public signing keys of this cluster's gateways, which verifiers trust (docs/dev/gateway-signed-requests.md). Unlike the registry they outlive the replica that registered them, by the longest token lifetime, so a departed replica's tokens keep verifying.

Proto Definition

// Ephemeral, in-memory registry of long-lived services (nursery, etc).
// Members heartbeat periodically; meta evicts stale entries on TTL.
// On meta restart the registry is empty — services re-register on next heartbeat.
service ClusterService {
  // Register a member and refresh its heartbeat in one call. Idempotent;
  // repeated calls update last_heartbeat. Members should call this
  // periodically (e.g. every 5s) to remain in the registry.
  rpc RegisterMember(RegisterMemberRequest) returns (RegisterMemberResponse);

  // List members of a given kind that are currently registered (not evicted).
  // A query worker below the vector-index floor is left out: this is the pool
  // coordinators draw workers from. It stays registered and in ListVidxLevels.
  rpc ListMembers(ListMembersRequest) returns (ListMembersResponse);

  // Remove a registration. Best-effort, called on graceful shutdown to close
  // the up-to-one-TTL window in which a departed member still looks alive.
  // Idempotent: removing an absent slot succeeds with removed=false. Metas
  // that predate this RPC answer UNIMPLEMENTED; callers must ignore that.
  rpc DeregisterMember(DeregisterMemberRequest) returns (DeregisterMemberResponse);

  // Every live replica that opens segments, with the vector-index format
  // level it last reported: nurseries from their poll (or, while they have no
  // fresh poll, their service registration), query workers from
  // their lane registration, and janitors and query front ends (which may read
  // segments in process) from their service registration.
  rpc ListVidxLevels(ListVidxLevelsRequest) returns (ListVidxLevelsResponse);

  // The public signing keys of this cluster's gateways, which verifiers trust
  // (docs/dev/gateway-signed-requests.md). Unlike the registry they outlive the
  // replica that registered them, by the longest token lifetime, so a departed
  // replica's tokens keep verifying.
  rpc ListGatewayKeys(ListGatewayKeysRequest) returns (ListGatewayKeysResponse);
}

// Kind of cluster member. Same QWS code can register as either lane —
// the kind tells QC which work-scope the member can handle. Unknown
// values are treated as MEMBER_KIND_UNSPECIFIED.
enum MemberKind {
  MEMBER_KIND_UNSPECIFIED = 0;
  // QWS in the cloud-segment lane: scans S3-resident segments via cache_server.
  MEMBER_KIND_QWS_CLOUD = 2;
  // QWS in the nursery-local lane: scans nursery-local .ttseg files via
  // NurseryLocalCache; no S3 credentials. Today registered by the nursery
  // service itself; in Phase 8.4 split into a separate qws child.
  MEMBER_KIND_QWS_NURSERY = 1;

  // Service members below. These are fleet inventory only: meta never dials
  // them and cluster_pool never pools them (its Hello probe understands only
  // the two QWS lanes above). They exist so a consumer can enumerate what is
  // running, where, and at which version without the Kubernetes API.
  MEMBER_KIND_META = 3;
  MEMBER_KIND_INGEST = 4;
  // The nursery *service* (its HTTP/metrics listener), distinct from the
  // MEMBER_KIND_QWS_NURSERY lane record the same process also registers.
  MEMBER_KIND_NURSERY_SERVICE = 5;
  // The query service's HTTP/gRPC front end, distinct from the QWS lane record
  // its embedded qws child registers under the same node id.
  MEMBER_KIND_QUERY_FRONTEND = 6;
  MEMBER_KIND_JANITOR = 7;
  MEMBER_KIND_GATEWAY = 8;
  MEMBER_KIND_PERMISSIONS = 9;
  // Defined so the numbering is stable, but nothing registers under this today:
  // ui is the consumer of the registry rather than a member of it, and it
  // scrapes its own /metrics over loopback.
  MEMBER_KIND_UI = 10;
}

// A port a member serves, named rather than numbered so a consumer never has
// to carry a per-service port table. "grpc" | "http" | "metrics" | "health";
// one member may serve several names on the same port.
message NamedPort {
  string name = 1;
  uint32 port = 2;
}

// Pod-shaped facts a member declares about itself, so a consumer can build a
// scrape target (or an inventory row) without asking the Kubernetes API. Every
// field is empty when the member is not running under Kubernetes; a consumer
// must skip a record with an empty pod_ip rather than invent an address.
message MemberInfo {
  string pod_name = 1;
  string pod_ip = 2;
  string node_name = 3;
  string namespace = 4;
  // Berserk version and full git sha of the running binary, from the
  // /etc/bzrk/build-info image layer. "dev" / "unknown" outside a built image.
  string version = 5;
  string git_commit = 6;
  repeated NamedPort ports = 7;
  // Empty means "/metrics".
  string metrics_path = 8;
}

message RegisterMemberRequest {
  // Stable identity for this member (e.g. nursery_node_id). Re-registering
  // with the same id replaces the previous record.
  string id = 1;
  MemberKind kind = 2;
  // gRPC endpoint other services can dial (e.g. "http://nursery-0:9531").
  string endpoint = 3;
  // Relative capacity of this member, used by the QC's partition_ring to
  // route proportionally more partitions to stronger nodes.
  //
  // NOT a CPU count, despite earlier versions having been one. It is
  // sqrt(effective vCPUs * total L2+L3 cache in MiB): query work is
  // memory-bound, so throughput tracks the memory subsystem rather than core
  // count, and cache is the part of that a process can observe. The vCPU term
  // is cgroup-aware, so under k8s it is the pod's CPU limit rather than the
  // host's full capacity. Only the ratio between members is meaningful —
  // never read this as a number of cores. Zero means "unset by an old
  // client" — the QC treats this as weight=1 (equal share).
  // Meaningful only for the QWS lanes; service members send 0.
  uint32 weight = 4;
  // Declared on every heartbeat, not just the first: registration replaces the
  // whole record, so a client that omits this on some heartbeats erases it.
  MemberInfo info = 5;
  // Vector-index format level of the member's binary; 0 (absent) predates
  // release 1. Services that never open segments send 0.
  uint32 vidx_level = 6;
  // A gateway's public signing key (Ed25519, 32 bytes), sent on every
  // heartbeat. Empty for every other kind of member.
  bytes signing_key = 7;
}

message RegisterMemberResponse {
  // Cluster identity string this meta is configured with — set per
  // environment in helm values (e.g. "prod", "staging", a UUID).
  // Not crypto, not a secret — a deliberate mixup-detection
  // mechanism: services that talked to this meta tag their outbound
  // gRPC calls with this id; the receiver rejects mismatches. Cheap
  // way to catch "I accidentally pointed at the wrong meta_endpoint
  // and now I'm dialing the staging cluster's qws from a prod process."
  string cluster_id = 1;
}

message ListMembersRequest {
  // If MEMBER_KIND_UNSPECIFIED, all members are returned — query workers and
  // service members alike. Otherwise filtered to the requested kind. A kind
  // this meta does not recognise is an error rather than an unfiltered list,
  // so a client asking for something newer than the server hears about it.
  MemberKind kind = 1;
}

message MemberRecord {
  string id = 1;
  MemberKind kind = 2;
  string endpoint = 3;
  // Time of the last heartbeat received by meta, nanoseconds since unix epoch.
  int64 last_heartbeat_nanos = 4;
  // Relative capacity, set by the registering member at registration time
  // (see RegisterMemberRequest.weight). Used by the QC's partition_ring to
  // route proportionally more partitions to stronger nodes. Zero from
  // pre-weighting clients is treated as weight=1 by the consumer.
  uint32 weight = 5;
  // Empty from clients that predate it.
  MemberInfo info = 6;
}

message DeregisterMemberRequest {
  string id = 1;
  MemberKind kind = 2;
}

message DeregisterMemberResponse {
  // False when no such registration was present — the caller's desired state
  // holds either way.
  bool removed = 1;
}

message ListMembersResponse {
  repeated MemberRecord members = 1;
  // Same cluster id as RegisterMemberResponse.cluster_id — see there.
  string cluster_id = 2;
  // How long this registry has been accepting registrations, in nanoseconds.
  //
  // The registry is in-memory and starts empty, refilling as members heartbeat
  // over the following seconds. A young registry is therefore a PARTIAL one no
  // matter how complete it looks — it is non-empty and every record in it is
  // real, so nothing else about the response reveals it. A consumer that treats
  // the member list as the whole fleet must check this and wait.
  //
  // Measured by the server against its own clock, so no consumer needs to agree
  // with meta about what time it is. Zero from a meta that predates the field.
  int64 registry_age_nanos = 3;
}

// A replica that opens segments, as ListVidxLevels reports it.
enum VidxReplicaKind {
  VIDX_REPLICA_KIND_UNSPECIFIED = 0;
  VIDX_REPLICA_KIND_NURSERY = 1;
  VIDX_REPLICA_KIND_JANITOR = 2;
  VIDX_REPLICA_KIND_QWS_CLOUD = 3;
  VIDX_REPLICA_KIND_QWS_NURSERY = 4;
  // The query service itself, which reads segments when its transport is in
  // process.
  VIDX_REPLICA_KIND_QUERY = 5;
}

message ListVidxLevelsRequest {}

message VidxReplica {
  VidxReplicaKind kind = 1;
  // The replica's node id.
  string id = 2;
  // 0 for a replica whose binary predates release 1.
  uint32 level = 3;
  // Time of the replica's last report, nanoseconds since unix epoch.
  int64 last_report_nanos = 4;
  // The replica's pod, when it registered one; empty otherwise.
  string pod_name = 5;
}

message ListVidxLevelsResponse {
  repeated VidxReplica replicas = 1;
  // How long this registry has been accepting reports, in nanoseconds.
  int64 registry_age_nanos = 2;
  // The age the registry must reach before the list is complete: the longest
  // liveness window it filters by. Until then a live replica may simply not
  // have reported since meta started.
  int64 settles_after_nanos = 3;
  // The vector-index floor meta enforces: a nursery below it holds no lease, a
  // janitor below it is given no merge task, and a query worker below it is
  // left out of ListMembers. Replicas below it are still listed here. 0 from a
  // meta that predates the field, which enforces no floor.
  uint32 vidx_floor = 4;
}

message ListGatewayKeysRequest {}

message ListGatewayKeysResponse {
  // The issuer and audience a token must name: this meta's cluster id.
  string cluster_id = 1;
  // The keys, as a JWKS document (RFC 7517).
  string jwks = 2;
}

IngestionService

Authenticates ingest tokens and manages collector streams.

RPCs

RPCDescription
ReconcileIngesterThe one ingester RPC. The request carries actual state — and, on the cold path, new ingest tokens to authenticate — the response carries desired state plus a per-token outcome for each new token. A register is just a reconcile that happens to carry a token, so every register re-teaches meta the ingester's drains and assignments before anything is assigned.
CountStreamsCount ingest streams across the cluster (total + active/stopped). An admin/inspection call, not part of the ingest data path.
ListIngestStreamsEvery ingest stream, one summary row each. Leaner than NurseryService.GetFullRefresh, which a listing cannot afford: no coverage checkpoints (up to 1024 per table) and no token routing. It does carry a row per table the stream merged into, so the response grows with streams times their tables, and the ingester fields are capped at registration. That is a small response in practice, NOT a hard bound: nothing caps how many streams exist, and there is no pagination. An admin/inspection call, not part of the ingest data path.

Proto Definition

service IngestionService {
  // The one ingester RPC. The request carries actual state — and, on the cold
  // path, new ingest tokens to authenticate — the response carries desired
  // state plus a per-token outcome for each new token. A register is just a
  // reconcile that happens to carry a token, so every register re-teaches
  // meta the ingester's drains and assignments before anything is assigned.
  rpc ReconcileIngester(ReconcileIngesterRequest) returns (ReconcileIngesterResponse);

  // Count ingest streams across the cluster (total + active/stopped). An
  // admin/inspection call, not part of the ingest data path.
  rpc CountStreams(CountStreamsRequest) returns (CountStreamsResponse);

  // Every ingest stream, one summary row each. Leaner than
  // NurseryService.GetFullRefresh, which a listing cannot afford: no coverage
  // checkpoints (up to 1024 per table) and no token routing. It does carry a
  // row per table the stream merged into, so the response grows with streams
  // times their tables, and the ingester fields are capped at registration.
  //
  // That is a small response in practice, NOT a hard bound: nothing caps how
  // many streams exist, and there is no pagination. An admin/inspection call,
  // not part of the ingest data path.
  rpc ListIngestStreams(ListIngestStreamsRequest)
      returns (ListIngestStreamsResponse);
}

// Where a stream's objects go. Reserved tags 2 and 3 held a flush timeout and
// a segment-size cap that no writer ever read: batching is the ingester's own
// decision, made from its own config.
message StreamConfig {
  CloudObjectPrefix cloud_object_prefix = 1;
  reserved 2, 3;
}

message AssignedStream {
  string stream_id = 1;
  StreamConfig config = 2;
}

// Which streams one token writes to, as meta wants it.
message TokenAssignment {
  string token_id = 1;
  repeated string stream_ids = 2;
}

// A token the ingester holds, and the streams it writes it to. The plaintext is
// the proof: meta adopts nothing about a token the caller cannot show.
message HeldToken {
  string ingest_token = 1;
  repeated string stream_ids = 2;
}

message ReconcileIngesterRequest {
  reserved 1, 3;
  // A random value the ingester makes at start and shows only to meta. Meta
  // names the ingester by a hash of it, so only its holder can report as that
  // ingester or say anything about its streams.
  string ingester_key = 8;
  // The pod's fleet node id, the same it registers in the fleet registry
  // under, so the registry holds its pod name and version: for operators
  // only, the key above is the identity.
  string node_id = 2;
  // Recently-active tokens only. Doubles as the revalidation set: the tokens an
  // ingester writes for are exactly the tokens whose validity it needs.
  repeated HeldToken held = 9;
  // Streams whose upload pipe the ingester cannot keep up with. Only the ingest
  // node can see this; it is not a nursery-load signal. Meta reads only the
  // ingester's own streams here and in `draining_stream_ids`.
  repeated string saturated_stream_ids = 4;
  // Highest assignment_version this ingester has applied. Meta raises its
  // allocator to at least this, so a restarted meta whose clock stepped back
  // cannot issue versions the ingester would ignore as stale.
  uint64 applied_version = 5;
  // Streams this ingester is mid-drain on (BeginDrain sent, STOP not yet
  // written). Meta adopts these into its in-memory drain set — the restart
  // recovery for an intention it deliberately does not persist — so a
  // restarted meta cannot re-issue a stream whose STOP is coming.
  repeated string draining_stream_ids = 6;
  // Process-lifetime counters for operators. Meta derives rates between
  // consecutive samples and reads none of it when deciding where traffic goes.
  IngesterSample sample = 11;
  // Plaintext ingest tokens this ingester holds no assignment for (the cold
  // path). Authenticated per token; outcomes ride token_results. Because the
  // same request carries the drain mirror and held tokens, meta
  // adopts them before it assigns — a register can never be handed a stream
  // this ingester is still closing.
  //
  // Here and in `held`, an empty token is a sender that presented none. Meta
  // answers it with its default token when anonymous ingest is on, and
  // refuses it otherwise.
  repeated string new_tokens = 7;
}

// The per-token outcome for a new_tokens entry. Auth failures are results,
// not RPC errors: one bad token must not fail the reconcile it rode with.
message TokenResult {
  // Echoes the new_tokens entry, so the client can find its own outcome —
  // assignments are keyed by token_id, which the client does not know yet.
  string ingest_token = 1;
  oneof outcome {
    // UUID of the ingest token (from the meta database). The ingester uses
    // this as the key in CombinedSignals.token_signals so the plaintext
    // token is never persisted to S3. Its assignment is in `assignments`.
    string token_id = 2;
    TokenRejection rejection = 3;
  }
}

// A rejection is an auth verdict and nothing else: the token is invalid,
// revoked, or deleted. Capacity is not a token verdict — a valid token meta
// cannot place right now gets its token_id WITHOUT an assignment, and the
// absence of a route is the backpressure signal.
message TokenRejection {
  // For operators; the ingester never forwards it to the sender.
  string reason = 1;
}

// There is no revocation notice. A revoked token is dropped from `assignments`,
// which makes the ingester forget it and take the cold path on its next batch,
// where its token_results entry answers with the auth failure. Meta therefore
// says only what a token MAY do, never what it may no longer do.
// Cumulative since the process started, never a delta: the ingester's one
// in-flight exchange can time out and be resent, and a delta would be lost on
// the failure or counted twice on the resend. The identity changes with the
// process, so within one ingester a counter never resets.
message IngesterSample {
  // The ingester's clock. Meta uses it only for the interval between this
  // ingester's own samples, never against its own clock.
  int64 sampled_at_nanos = 1;
  // Requests ingest took responsibility for: durably in S3 in durable mode,
  // queued on a stream in fast mode. OTLP protobuf bytes before compression.
  uint64 accepted_requests = 2;
  uint64 accepted_records = 3;
  uint64 accepted_bytes_uncompressed = 4;
  // LZ4 batch bytes written to S3.
  uint64 accepted_bytes_compressed = 5;
  // Requests acknowledged with a fast-mode 202 whose data never reached S3,
  // by drop reason. The only silent loss.
  repeated ReasonCount lost_after_ack = 6;
  // Requests answered with an error the sender can act on, by kind:
  // unauthorized, throttled, unavailable, failed.
  repeated ReasonCount rejected = 7;
  // Payloads the OTLP validator refused.
  uint64 invalid_payloads = 8;
  // Bytes buffered across stream actors right now.
  uint64 buffer_bytes = 9;
  // Since the pod's most recent successful S3 write. Absent until it has one.
  optional uint64 last_upload_age_ms = 10;
  // When the counters began, on the ingester's clock: what every total above
  // is since. Not after `sampled_at_nanos`.
  int64 started_at_nanos = 11;
  // CPU time the process has used since it started, over every thread, and
  // the cores it may use (its cgroup quota, else the machine's). Meta divides
  // the CPU time between two samples by their interval and the cores.
  uint64 cpu_time_ms = 12;
  double available_cores = 13;
}

message ReconcileIngesterResponse {
  // Every stream this ingester should hold an opinion about: each stream any
  // assignment references, plus every stream meta is draining — carried,
  // token-less, until the nursery marks it stopped. Absence is never a
  // command: a live stream stays carried until stopped, so a response that
  // omits one can only ever cost a re-register, never data.
  repeated AssignedStream streams = 1;
  // Assignments reference only streams NOT listed in draining_stream_ids: a
  // token never routes to a stream that is closing.
  repeated TokenAssignment assignments = 2;
  // The close command, explicit: stop taking data, flush, write STOP. Always
  // a subset of `streams`. Draining is membership in meta's in-memory drain
  // set — an intention, not a durable fact.
  repeated string draining_stream_ids = 3;
  uint64 assignment_version = 4;
  uint32 reconcile_interval_ms = 5;
  // One entry per new_tokens entry, every request. An accepted token's
  // assignment is in `assignments` like any other.
  repeated TokenResult token_results = 6;
}

message CountStreamsRequest {}

message CountStreamsResponse {
  int64 total = 1;
  // A killed stream is neither active nor stopped: it takes no traffic, and its
  // tail is unknown until its nursery has fenced it. Counting it as either
  // hides the one number an operator wants, which is how many are still being
  // reclaimed.
  int64 active = 2;
  int64 stopped = 3;
  int64 killed = 4;
}

message ListIngestStreamsRequest {}

// One stream as an operator reads it. Every field is either a stream-level
// column or an aggregate over its table rows, so the row size is fixed.
message IngestStreamSummary {
  string stream_id = 1;
  // Unset means the stream has not stopped. There is no separate liveness
  // signal here: a stream that is not stopped may still be idle.
  optional int64 stopped_at_nanos = 2;
  // Offset the stop message itself sits at: everything below it is data, and
  // deletion through this offset removes the stop too. Set exactly when
  // stopped_at_nanos is.
  optional int64 stopped_offset = 3;
  string stream_group = 4;
  // Every table this stream has MERGED into, by name, with how far each got. A
  // row appears only on the owning nursery's first merge for that table, so a
  // young stream lists none even while data is arriving for it — this is merge
  // progress, not routing. The stream's own frontier is the least of these — NOT the deletion bound, since a min over rows cannot see a
  // table with no meta row (#4260); floor_offset is.
  //
  // Bounded by the tables in a database, so it stays a fixed cost per stream.
  // The per-table coverage checkpoints do NOT belong here: at 1024 apiece they
  // are what a listing cannot afford to carry.
  repeated IngestStreamTableProgress tables = 5;
  reserved 6;
  int64 floor_offset = 7;
  int64 deleted_offset = 8;
  // Best-effort ingest time near the deletion frontier. Observability only —
  // correctness rides on deleted_offset.
  int64 deleted_ingest_time_nanos = 13;
  int64 known_complete_through_nanos = 9;
  // Ingester identity as registered — the only thing tying a stream id back to
  // its sender. Nothing routes on it.
  reserved 11;
  optional string ingester_id = 12;
  // The pod of the ingester that owns the stream, while meta hears from it;
  // empty once it is gone or outside Kubernetes.
  string ingester_pod_name = 15;
  // Set when an operator killed the stream. It takes no traffic from that
  // moment; the row survives until its storage is reclaimed.
  optional int64 killed_at_nanos = 14;
}

message IngestStreamTableProgress {
  string table_name = 1;
  // Last merged offset for this (stream, table); -1 = nothing merged.
  int64 merged_offset = 2;
  // Owning database. A stream is keyed by ingester alone, so two tokens can
  // route it into same-named tables in different databases; the name alone does
  // not identify the row.
  string database_name = 3;
}

message ListIngestStreamsResponse {
  repeated IngestStreamSummary streams = 1;
}

IngestTokenService

Manages ingest tokens for authentication and signal routing.

RPCs

RPCDescription
CreateIngestTokenCreate a new token (plaintext shown only once)
ValidateIngestTokenValidate a token and return its routing
DeleteIngestTokenTombstone a token; the catalog row sits for the grace window so a fat-finger delete can be resurrected
ResurrectIngestTokenLift the tombstone on a token within the grace window
ListIngestTokensList all tokens
GetIngestTokenGet token details by ID
UpdateIngestTokenRoutingUpdate signal routing for a token

Proto Definition

// All RPCs except ValidateIngestToken require `database_id`. Tokens are
// auth credentials, so every operation explicitly carries the database
// scope. Validate is the exception because the ingest path receives only
// a plaintext token header — the response carries `database_id` outward.
service IngestTokenService {
  rpc CreateIngestToken(CreateIngestTokenRequest) returns (CreateIngestTokenResponse);
  rpc ValidateIngestToken(ValidateIngestTokenRequest) returns (ValidateIngestTokenResponse);
  // New stream registrations are rejected at tombstone time, before the grace window expires.
  rpc DeleteIngestToken(DeleteIngestTokenRequest) returns (DeleteIngestTokenResponse);
  rpc ResurrectIngestToken(ResurrectIngestTokenRequest) returns (ResurrectIngestTokenResponse);
  rpc ListIngestTokens(ListIngestTokensRequest) returns (ListIngestTokensResponse);
  rpc GetIngestToken(GetIngestTokenRequest) returns (GetIngestTokenResponse);
  rpc UpdateIngestTokenRouting(UpdateIngestTokenRoutingRequest) returns (UpdateIngestTokenRoutingResponse);
}

message IngestTokenRouting {
  string traces_table_id = 1;
  string logs_table_id = 2;
  string metrics_table_id = 3;
}

message IngestTokenInfo {
  string id = 1;
  string name = 2;
  string created_at = 3;
  IngestTokenRouting routing = 4;
  string token_hint = 5;  // last 4 chars of the plaintext token
  string database_id = 6;
  // Absent for live rows.
  Tombstone tombstone = 7;
}

message CreateIngestTokenRequest {
  string database_id = 1;
  string name = 2;
  // All 3 routing targets are required. A "logs-only" token points all 3
  // signals at the logs table; wrong-signal data lands there (schema-on-read
  // absorbs it). Tightening to per-signal optional is a future, non-breaking
  // change.
  string traces_table_id = 3;
  string logs_table_id = 4;
  string metrics_table_id = 5;
}

message CreateIngestTokenResponse {
  string plaintext_token = 1;  // shown only once
  IngestTokenInfo token = 2;
}

message ValidateIngestTokenRequest {
  string plaintext_token = 1;
}

message ValidateIngestTokenResponse {
  string token_id = 1;
  IngestTokenRouting routing = 2;
  string database_id = 3;
}

message DeleteIngestTokenRequest {
  string database_id = 1;
  string token_id = 2;
}

message DeleteIngestTokenResponse {
  // Carries the grace window: grace = delete_at - tombstoned_at.
  Tombstone tombstone = 1;
  // Ingesters learn of the delete at their next reconcile with meta, so they
  // normally accept data sent with the token for at most this long.
  uint32 ingest_stops_within_seconds = 2;
}

message ResurrectIngestTokenRequest {
  string database_id = 1;
  string token_id = 2;
}

message ResurrectIngestTokenResponse {
  IngestTokenInfo token = 1;
}

message ListIngestTokensRequest {
  string database_id = 1;
  bool include_deleted = 2;
}

message ListIngestTokensResponse {
  repeated IngestTokenInfo tokens = 1;
}

message GetIngestTokenRequest {
  string database_id = 1;
  string token_id = 2;
  bool include_deleted = 3;
}

message GetIngestTokenResponse {
  IngestTokenInfo token = 1;
}

message UpdateIngestTokenRoutingRequest {
  string database_id = 1;
  string token_id = 2;
  IngestTokenRouting routing = 3;
}

message UpdateIngestTokenRoutingResponse {
  // empty on success
}

NurseryService

Manages ingest stream lifecycle, merged segment registration, and offset tracking.

RPCs

RPCDescription
GetNurseryRefreshGet the caller-scoped refresh: owned streams, tables, and token routing
MarkStreamStoppedMark a stream as stopped (client disconnect or timeout)
RegisterMergedSegmentsReturns the post-commit refresh from the same snapshot as the commit — poll-equivalent; the nursery applies it exactly like a poll. Assigned segment versions are not returned: no caller consumes them (coverage is derived from checkpoints inside the refresh).
UpdateDeletedOffsetUpdate the deleted offset after cleaning baby segments
AdvanceFloorOffsetTHE floor write path: the owning nursery asserts the stream-level floor it can prove from its own state, outside any commit. The floor is monotonic and every consumer is conservative under lag, so it never needs to ride a commit — the brain reconciles provable vs persisted whenever it likes. Owner-fenced, forward-only; a stale lower value is a no-op.
DeleteIngestStreamTableRemove a fully-processed table entry from a stream
GetFullRefreshAdmin/debug read: the refresh over every stream regardless of owner
ListStreamGroupsOperator read of stream-group ownership, owner text UNPARSED. Deliberately so: a group whose owner is not a uuid is the one an operator needs to see, and parsing it is exactly what fails every other read of that row.
ReleaseStreamGroupOwnerOperator escape-hatch: clear a stream group's owner, so the coordinator reassigns it to a live nursery on its next tick. Compare-and-swap on the owner text the caller saw rather than a blind write, so a concurrent operator or an already-healed row is never clobbered. Not nursery-fenced.
KillIngestStreamOperator last resort: give up on a stream whatever state it is in, and reclaim its storage. For a stream that can no longer drain — a merged frontier past its stop tail, say — nothing else releases it, so it holds an active-stream slot and its group's placement weight forever. Unconditional by design: the state of a broken stream is exactly what cannot be trusted to gate its own removal. Marks the row; its owning nursery then fences the prefix and the ordinary cleanup reclaims it. Destroys unmerged data and cannot be undone; already-merged segments are untouched. Idempotent. Not nursery-fenced.
CompleteKillThe owning nursery has thrown away everything it held for a killed stream and found the offset nothing can be written above. Forces the floor there and drops the stream's table rows, leaving only deletion and the reaper. Idempotent, unfenced and unvalidated: kill exists for streams whose bookkeeping is already wrong, so this must never refuse.

Proto Definition

service NurseryService {
  rpc GetNurseryRefresh(GetNurseryRefreshRequest) returns (NurseryRefresh);
  rpc MarkStreamStopped(MarkStreamStoppedRequest)
      returns (MarkStreamStoppedResponse);
  // Returns the post-commit refresh from the same snapshot as the commit —
  // poll-equivalent; the nursery applies it exactly like a poll. Assigned
  // segment versions are not returned: no caller consumes them (coverage is
  // derived from checkpoints inside the refresh).
  rpc RegisterMergedSegments(RegisterMergedSegmentsRequest)
      returns (NurseryRefresh);
  rpc UpdateDeletedOffset(UpdateDeletedOffsetRequest)
      returns (UpdateDeletedOffsetResponse);

  // THE floor write path: the owning nursery asserts the stream-level floor
  // it can prove from its own state, outside any commit. The floor is
  // monotonic and every consumer is conservative under lag, so it never needs
  // to ride a commit — the brain reconciles provable vs persisted whenever it
  // likes. Owner-fenced, forward-only; a stale lower value is a no-op.
  rpc AdvanceFloorOffset(AdvanceFloorOffsetRequest)
      returns (AdvanceFloorOffsetResponse);
  rpc DeleteIngestStreamTable(DeleteIngestStreamTableRequest)
      returns (DeleteIngestStreamTableResponse);

  // Admin/debug read of EVERY stream (all owners, all groups), fully enriched
  // like the nursery poll. Unscoped — not the nursery's operational poll; for
  // operators and tests inspecting cluster-wide stream state.
  rpc GetFullRefresh(GetFullRefreshRequest) returns (NurseryRefresh);

  // Operator read of stream-group ownership, owner text UNPARSED. Deliberately
  // so: a group whose owner is not a uuid is the one an operator needs to see,
  // and parsing it is exactly what fails every other read of that row.
  rpc ListStreamGroups(ListStreamGroupsRequest) returns (ListStreamGroupsResponse);

  // Operator escape-hatch: clear a stream group's owner, so the coordinator
  // reassigns it to a live nursery on its next tick. Compare-and-swap on the
  // owner text the caller saw rather than a blind write, so a concurrent
  // operator or an already-healed row is never clobbered. Not nursery-fenced.
  rpc ReleaseStreamGroupOwner(ReleaseStreamGroupOwnerRequest)
      returns (ReleaseStreamGroupOwnerResponse);

  // Operator last resort: give up on a stream whatever state it is in, and
  // reclaim its storage. For a stream that can no longer drain — a merged
  // frontier past its stop tail, say — nothing else releases it, so it holds an
  // active-stream slot and its group's placement weight forever. Unconditional
  // by design: the state of a broken stream is exactly what cannot be trusted to
  // gate its own removal. Marks the row; its owning nursery then fences the
  // prefix and the ordinary cleanup reclaims it. Destroys unmerged data and
  // cannot be undone; already-merged segments are untouched. Idempotent. Not
  // nursery-fenced.
  rpc KillIngestStream(KillIngestStreamRequest)
      returns (KillIngestStreamResponse);
  // The owning nursery has thrown away everything it held for a killed stream
  // and found the offset nothing can be written above. Forces the floor there
  // and drops the stream's table rows, leaving only deletion and the reaper.
  // Idempotent, unfenced and unvalidated: kill exists for streams whose
  // bookkeeping is already wrong, so this must never refuse.
  rpc CompleteKill(CompleteKillRequest) returns (CompleteKillResponse);
}

message GetNurseryRefreshRequest {
  // nursery_node_id of the polling nursery — the ingest-domain soft-lease key.
  // Scoped read: returns only streams in groups this nursery owns. An empty or
  // unknown id owns nothing and gets an empty set; liveness is recorded only
  // for a non-empty id.
  string nursery_id = 1;
  // Load accumulated since the last report, riding whichever poll closes the
  // window. Absent on polls that do not close one.
  NurseryLoadReport load = 2;
  // Vector-index format level of this nursery's binary; 0 (absent) predates
  // release 1.
  uint32 vidx_level = 3;
  // Watermarks and health, on every poll. Monitoring only: nothing meta
  // decides reads it, and the coordinator keeps receiving W in-band.
  NurseryReport report = 4;
}

// What one nursery observed over `interval_ms`. Every figure meta needs comes
// out of `throughput` by aggregation, since meta knows which ingester owns each
// stream. These are delivered bytes: under fast_drop the dropped data never
// reaches S3, and that shortfall surfaces as ingester saturation instead.
message NurseryLoadReport {
  // Window covered. Zero means the report carries no throughput.
  uint64 interval_ms = 1;
  repeated ThroughputRow throughput = 2;
  // Rises only when merging is losing ground, unlike volume measures which also
  // rise on a nursery that is keeping up.
  uint64 oldest_unmerged_age_ms = 3;
  // How far each stream's follower ran behind the data it processed this
  // window. Throughput alone inverts under follow-side overload — a drowning
  // follower delivers FEWER bytes — so lag is what keeps a struggling nursery
  // from reading as the coolest placement target. Absent = nothing processed.
  repeated FollowLagRow follow_lags = 4;
  // Wall time inbound fetch was held by backpressure this window: the
  // nursery's own overload defense, reported rather than kept to itself.
  uint64 fetch_paused_ms = 5;
  // The oldest work no merge has claimed yet, as a multiple of this nursery's
  // OWN merge deadline. Dimensionless deliberately: the deadline is nursery
  // config, so meta must not hold a copy of it — what crosses is "this much
  // later than I intend to be", which means the same on either side however
  // each is tuned. Segments inside a running merge are excluded, or one long
  // merge would look like a queue that is not there.
  double oldest_unmerged_deadlines = 6;
  // The cores this nursery may use: its cgroup quota, the machine's count, or
  // what its operator said. Capacity is a share of this on either side.
  double available_cores = 7;
  // Share of those cores the process kept busy this window; 1.0 is every core
  // the whole window. Reported as a share rather than a time so nurseries of
  // any size compare alike.
  double cpu_utilisation = 8;
  // S3 object bytes fetched this window, fresh downloads only: what the data
  // cost on the wire, where `throughput` is the raw resource bytes inside.
  uint64 s3_bytes = 9;
}

message ThroughputRow {
  string stream_id = 1;
  string token_id = 2;
  string table_id = 3;
  uint64 bytes = 4;
}

message FollowLagRow {
  string stream_id = 1;
  // Max over the window of (processing time - the object's ingest time),
  // fresh downloads only — replayed local files would fake a huge lag. The
  // deepest excursion: a diagnostic, not what a decision reads, because one
  // slow upload pins it for the whole window.
  uint64 lag_ms = 2;
  // How many of the window's fresh offsets were finished a minute or more
  // past their ingest time, out of how many there were. A fraction is what a
  // decision reads: a follower losing ground is late on most of what it
  // processes, where one old object is late on one.
  uint32 offsets_processed_late = 3;
  uint32 offsets_processed = 4;
}

// What one nursery can say about itself as of one poll.
message NurseryReport {
  // The nursery's clock; meta compares it only with this nursery's others.
  int64 sampled_at_nanos = 1;
  // Every group this nursery knows a stream in, drained ones included.
  repeated GroupWatermark group_watermarks = 2;
  // Meta refreshes discarded in a row. Routing and W are frozen while it
  // rises; the nursery's gauge of the same name alerts on it.
  uint32 refresh_discards_consecutive = 3;
  // Streams owed a read whose follower has read nothing yet.
  uint32 streams_owed_unread = 4;
  // Process-lifetime counts, cumulative like the metrics beside them, so a
  // resent poll cannot double-count. Meta differences two reports.
  uint64 data_errors = 5;
  uint64 infra_errors = 6;
  // When the counts began, on the nursery's clock: what they are since. Not
  // after `sampled_at_nanos`.
  int64 started_at_nanos = 8;
  // The poll snapshot bound the watermarks were computed from (the cap W is
  // held under), on meta's database clock; 0 before the first applied poll.
  // Lets meta see whether the report already knew a group's current owner.
  int64 applied_snapshot_nanos = 7;
}

// A group's completeness watermark W.
message GroupWatermark {
  string group_id = 1;
  // Absent before the nursery's first applied poll, when it vouches nothing.
  optional int64 watermark_nanos = 2;
}

message GetFullRefreshRequest {}

// The cloud lane's coverage of a (stream, table) at one segment version. The
// lowest entry in a list is a floor: no query cut below it can be served.
message OffsetCheckpoint {
  // The MVCC version of the merged segment
  int64 segment_version = 1;
  // Merged frontier at that version; -1 means the cloud held nothing
  int64 stream_offset = 2;
  // When meta wrote this entry, on meta's clock. This is when the offsets it
  // accounts for entered the cloud lane, and it is what every local-file grace
  // in the nursery is timed against.
  int64 written_at_nanos = 3;
}

// A best-effort (stream_offset, ingest_time) sample used to pick the ~1h
// "old enough to delete" boundary for a stream's shared blobs.
message OffsetTimestamp {
  int64 stream_offset = 1;
  int64 ingest_time_nanos = 2;
}

// Table-specific routing information within a stream
message IngestStreamTableInfo {
  string table_id = 1;
  // Last merged offset. Nursery should fetch from merged_offset + 1
  int64 merged_offset = 2;
  // Ingest timestamp of the last merged baby segment (nanoseconds since unix epoch)
  // NOTE: Source precision is seconds (from S3 last_modified), sub-second portion is always zero
  // Used by query engine to determine which segments to query
  int64 merged_ingest_time_nanos = 3;
  // Table stream creation time in nanoseconds since unix epoch
  int64 created_at_nanos = 6;
  // S3 location for uploading merged segments: endpoint, bucket, path prefix
  // Path format: tables/<table_id>/segments/
  CloudObjectPrefix cloud_merged_segments_prefix = 7;
  // Cloud-coverage checkpoints, ascending by segment_version. Pruned from the
  // bottom only, so the lowest entry is always a valid coverage floor.
  repeated OffsetCheckpoint offset_checkpoints = 8;
}

// External stream information - matches two-table design (ingest_streams + ingest_stream_tables)
message IngestStreamInfo {
  string stream_id = 1;
  // S3 location for baby segments: endpoint, bucket, path prefix
  // Path format: ingest-stream/<stream_id>/
  CloudObjectPrefix cloud_stream_prefix = 2;
  // Set once the stream is stopped: no more baby segments will arrive, and the
  // nursery can release it once it has drained to the stop.
  optional StreamStop stop = 3;
  // Table-specific routing information for this stream
  repeated IngestStreamTableInfo tables = 5;
  // Durable completeness frontier (nanos): min over this stream's tables of
  // merged_ingest_time_nanos, or its creation time when it has no tables yet.
  // The nursery's ingest watermark floor for a stream it hasn't fetched.
  int64 known_complete_through_nanos = 6;
  // Highest stream offset whose shared baby-segment blobs are deleted from S3
  // (default -1). Nursery only deletes offsets <= min(merged_offset) over tables.
  int64 deleted_offset = 7;
  // Best-effort ingest time near the deletion frontier (nanoseconds since unix
  // epoch). Observability only — correctness rides on deleted_offset.
  int64 deleted_ingest_time_nanos = 8;
  // Sparse age-ladder samples for picking the deletion boundary.
  repeated OffsetTimestamp offset_timestamps = 9;
  // UUID of the stream group this stream belongs to (`stream_groups.id`). The
  // nursery buckets per-group `W` and filters served segments to its assigned
  // groups by this.
  string stream_group = 11;
  // Stream-level floor (default -1 = never): every offset at or below it is
  // merged or deliberately discarded for EVERY table in the stream, including
  // tables with no meta row. Adoption replay starts above it; deleted_offset
  // never passes it. See issue #4260.
  int64 floor_offset = 12;
  // Set when an operator killed the stream: its unmerged data is forfeit, and
  // the nursery is to stop reading it, fence the prefix with a STOP and let the
  // existing cleanup reclaim it. The stream is still listed — filtering it out
  // would strand the writer's token with nobody left to clean up. See #5672.
  optional int64 killed_at_nanos = 13;
}

// Where a stream ended. The two travel together because the tail is what
// proves the stream drained — a stop time without one asserts an end nobody
// can check.
message StreamStop {
  // Nanoseconds since unix epoch.
  int64 at_nanos = 1;
  // Offset of the stop message. A fully-merged stream has merged_offset == offset.
  int64 offset = 2;
}

message ListStreamGroupsRequest {}

message StreamGroupInfo {
  string group_id = 1;
  // Owner as stored, unparsed — may be text no NodeId can parse. Empty = unowned.
  string owner = 2;
  // Streams in the group that have not stopped.
  int64 active_streams = 3;
  // Every stream in the group, stopped or not.
  int64 total_streams = 4;
}

message ListStreamGroupsResponse {
  repeated StreamGroupInfo groups = 1;
}

message ReleaseStreamGroupOwnerRequest {
  string group_id = 1;
  // Owner text the caller saw; the release applies only while it still matches.
  // Empty expects an already-unowned group.
  string expected_owner = 2;
}

message ReleaseStreamGroupOwnerResponse {
  // False when the owner changed between read and release — the CAS lost, and
  // the caller should re-read rather than retry blindly.
  bool released = 1;
}

message KillIngestStreamRequest {
  string stream_id = 1;
}

message CompleteKillRequest {
  string stream_id = 1;
  // Nothing can be written at or above this. The floor is forced here, so
  // everything below is deliberately discarded.
  int64 stop_offset = 2;
}

message CompleteKillResponse {
  // Where the floor ended up, so the caller can log what the kill cost.
  int64 floor_offset = 1;
  // How many table rows went. Zero on a retry.
  uint64 tables_dropped = 2;
}

message KillIngestStreamResponse {
  // Cloud prefix the stream's blobs live under, so an operator watching the
  // reclamation knows which one to watch.
  string cloud_stream_prefix = 2;
  // Whether meta had recorded a stop. Reported so the caller learns it killed a
  // stream a writer may still hold; never a precondition.
  bool was_stopped = 3;
  // False when the stream was already killed. A retry is a success. When the
  // kill settled is on the row, which ListIngestStreams reports.
  bool newly_killed = 5;
}

// Per-token routing: maps each signal type to a target table.
// Keyed by token_id (UUID), matching the key in CombinedSignals.token_signals.
message IngestTokenRoutingInfo {
  // UUID of the ingest token (from the meta database).
  // Matches the key used in CombinedSignals.token_signals.
  string token_id = 1;
  string traces_table_id = 2;
  string logs_table_id = 3;
  string metrics_table_id = 4;
  // PR 5: token's owning database. Used by nursery to scope
  // `bzrk.table` attribute lookups and reject cross-DB routing.
  string database_id = 5;
}

// One authoritative refresh snapshot: everything the nursery needs to bring
// its view in sync with meta. The poll and GetFullRefresh response body, and
// carried on the register response — one shape, one application path.
message NurseryRefresh {
  repeated IngestStreamInfo streams = 1;
  // All known tables (name → id mapping for routing resolution).
  // Each `TableInfo.database_id` scopes the table to a single
  // database, which is the only legal target for that table's
  // routing (cross-database routing is impossible by construction).
  repeated TableInfo tables = 2;
  // Active ingest token routing for token-based signal routing.
  repeated IngestTokenRoutingInfo token_routing = 3;
  // Tag 5 carried `stream_list_complete_through_nanos`, a bound over the
  // stream list alone, read in its own transaction. The bound below vouches
  // for tables and token routing too, so it must not be readable from a meta
  // that never co-snapshotted them: an absent bound has to stay absent.
  reserved 5;
  // `streams`, `tables` and `token_routing` were read in ONE snapshot and are
  // complete through this time (nanos): a live entry absent from the response
  // was created strictly after it. Always set and positive — the nursery
  // rejects a refresh without it. Completeness only, not ordering — it caps
  // the nursery's watermark and proves an unresolvable entry never existed.
  int64 snapshot_complete_through_nanos = 7;
  // Global MVCC segment version (`counters.segment_version`) read in the same
  // snapshot. The nursery stamps its published query view with it
  // (`applied_segment_version`) for the read-path decline gate.
  uint64 current_segment_version = 6;
  // The Postgres snapshot the body was read under (`pg_current_snapshot()`
  // text form). The nursery orders refreshes by snapshot inclusion (#4253),
  // and rejects a refresh without it.
  string refresh_order_snapshot = 8;
  // Set only on the GetNurseryRefresh poll, while vector-index writes are
  // open, to a level-2 nursery. Absent means no new-format build may start.
  VidxWriteGrant vidx_write_grant = 9;
}

// Reason why a stream was stopped
enum StopReason {
  STOP_REASON_UNSPECIFIED = 0;
  // Client (ingester) wrote a stop message
  STOP_REASON_CLIENT = 1;
  // Nursery detected inactivity timeout
  STOP_REASON_TIMEOUT = 2;
}

message MarkStreamStoppedRequest {
  string stream_id = 1;
  // When the stream was stopped (nanoseconds since unix epoch)
  int64 stopped_at_nanos = 2;
  // Final processed offset in the stream
  int64 final_offset = 3;
  // Reason for stopping
  StopReason reason = 4;
  // Calling nursery's node id; meta fences the mutation on it (only the live
  // owner of the stream's group may stop it). Required — empty => INVALID_ARGUMENT.
  string caller_nursery_id = 5;
}

message MarkStreamStoppedResponse {}

// Per-stream offset info for merged segment registration
message StreamOffsetInfo {
  string stream_id = 1;
  string table_id = 2;
  // New merged_offset after this merge (max baby segment offset included)
  int64 new_merged_offset = 3;
  // Ingest timestamp (S3 last_modified) of the highest-offset baby segment
  // NOTE: S3 provides second-level precision, sub-second portion is always zero
  int64 ingest_time_nanos = 4;
  // What merged_offset was when nursery selected segments for merge.
  // Meta validates this matches current value to detect conflicts.
  int64 start_merged_offset = 5;
  // Lowest baby-segment offset included in this merge for this (stream,
  // table). Meta rejects the batch unless it lies strictly above the
  // committed merged_offset, which is what stops a re-merge from
  // republishing rows the frontier already covers.
  //
  // Required on `MergeRegistration.stream_offsets`; must be absent on
  // `idle_advances`, which carry no segments. Explicitly `optional` so an
  // absent value is distinguishable from offset 0.
  optional int64 first_offset = 6;
}

// One merged segment registration: a new segment plus the per-stream offset
// advances for streams that contributed data to it.
message MergeRegistration {
  CreateSegment segment = 1;
  // Per-stream offset advance for streams that contributed segments to this merge.
  // Each item's start_merged_offset must equal the current merged_offset for that
  // (stream, table). Within a batch, items are applied in order, so item N+1's
  // start_merged_offset must equal item N's new_merged_offset (for the same stream).
  repeated StreamOffsetInfo stream_offsets = 2;
}

// Register one or more merged segments for a single table, atomically.
//
// All merges in the batch must share the same table_id (server enforced),
// and that table must live in `database_id` (server enforced). `merges`
// are applied in order in one Postgres transaction; on any failure the
// whole transaction rolls back. After all merges, `idle_advances` are
// applied — these advance merged_offset for streams that didn't contribute
// segments to any merge in this batch (e.g. streams whose follower
// processed keep-alives or a stop message). Idle-advance checkpoints
// reuse the last allocated segment version in the batch.
message RegisterMergedSegmentsRequest {
  // Database that owns the batch's table. Required. Server rejects with
  // INVALID_ARGUMENT if `database_id` does not own the batch's table_id.
  string database_id = 3;
  // Must be non-empty.
  repeated MergeRegistration merges = 1;
  // Optional. Applied last in the same transaction.
  repeated StreamOffsetInfo idle_advances = 2;
  // Idempotency key: a stable per-batch id, reused across retries of the same
  // batch. Meta uses it to make registration replay-safe — a retry after a
  // committed-but-lost response returns the original result instead of
  // re-applying or wedging on the unique storage-path index.
  string register_request_id = 4;
  // Node id of the calling nursery (cluster-registry MemberId). Meta fences the
  // register on it: only the live owner of every group the batch spans may
  // publish. Required — empty => INVALID_ARGUMENT.
  string caller_nursery_id = 5;
}

message AdvanceFloorOffsetRequest {
  message Proposal {
    string stream_id = 1;
    // Proposed floor. Meta clamps it to what it can locally prove safe (min
    // merged_offset over the stream's existing table rows, and stopped_offset
    // when set) and ignores regressions — forward-only.
    int64 floor_offset = 2;
  }
  // At most one proposal per stream — duplicates => INVALID_ARGUMENT, as does
  // an empty batch. Streams the caller does not own (or that no longer exist)
  // are skipped and omitted from the response: ownership moves are an
  // expected race, and the rest of the batch must not be discarded for one.
  repeated Proposal proposals = 1;
  // Calling nursery's node id; meta fences every mutation on it. Required —
  // empty => INVALID_ARGUMENT.
  string caller_nursery_id = 2;
}

message AdvanceFloorOffsetResponse {
  message Committed {
    string stream_id = 1;
    // The committed floor after the advance — the proposal clamped to what
    // meta could prove safe (or the old floor, if the proposal was stale).
    // Callers gating destructive work on the floor must use this, never the
    // value they proposed.
    int64 floor_offset = 2;
  }
  repeated Committed committed = 1;
}

message UpdateDeletedOffsetRequest {
  string stream_id = 1;
  int64 deleted_offset = 3;
  // Best-effort ingest time near the deletion frontier (nanoseconds since unix
  // epoch). Observability only — correctness rides on deleted_offset.
  int64 deleted_ingest_time_nanos = 4;
  // Calling nursery's node id; meta fences the mutation on it. Required — empty
  // => INVALID_ARGUMENT.
  string caller_nursery_id = 5;
}

message UpdateDeletedOffsetResponse {}

message DeleteIngestStreamTableRequest {
  string stream_id = 1;
  string table_id = 2;
  // The current (highest) offset nursery is processing for this stream.
  // Meta validates this is >= the table's merged_offset as a sanity check.
  int64 current_offset = 3;
  // Nursery's view of the table's merged_offset.
  // Meta validates this matches its own merged_offset and deleted_offset
  // to catch stale nursery state.
  int64 merged_offset = 4;
  // Calling nursery's node id; meta fences the mutation on it. Required — empty
  // => INVALID_ARGUMENT.
  string caller_nursery_id = 5;
}

message DeleteIngestStreamTableResponse {}

MergeTaskService

Coordinates segment merge tasks between the janitor and merge workers.

RPCs

RPCDescription
PollForSegmentMergeTaskWorker polls for an available merge task
CompleteSegmentMergeTaskWorker reports successful merge with the new segment
FailSegmentMergeTaskWorker reports merge failure
ReleaseSegmentMergeTaskRelease a claimed task back to the queue without failing it (the worker is shutting down mid-merge, before anything was registered in meta). Unlike Fail it does not delete the task — it stays queued for immediate re-claim.

Proto Definition

service MergeTaskService {
  rpc PollForSegmentMergeTask(PollForSegmentMergeTaskRequest) returns (PollForSegmentMergeTaskResponse);
  rpc CompleteSegmentMergeTask(CompleteSegmentMergeTaskRequest) returns (CompleteSegmentMergeTaskResponse);
  rpc FailSegmentMergeTask(FailSegmentMergeTaskRequest) returns (FailSegmentMergeTaskResponse);
  // Release a claimed task back to the queue without failing it (the worker is
  // shutting down mid-merge, before anything was registered in meta). Unlike
  // Fail it does not delete the task — it stays queued for immediate re-claim.
  rpc ReleaseSegmentMergeTask(ReleaseSegmentMergeTaskRequest) returns (ReleaseSegmentMergeTaskResponse);
}

message PollForSegmentMergeTaskRequest {
  string worker_id = 1;
  // Vector-index format level of the worker's binary; 0 (absent) predates
  // release 1. Janitors are listed from their fleet registration; this field
  // refuses below-floor polls, routes tasks, and tells the write gate a
  // level-2 janitor is taking work.
  uint32 vidx_level = 2;
}

message PollForSegmentMergeTaskResponse {
  optional SegmentMergeTaskInfo task = 1;
  // Present while vector-index writes are open, to a level-2 janitor, whether
  // or not a task was claimed. Absent means no new-format build may start.
  VidxWriteGrant vidx_write_grant = 2;
}

message CompleteSegmentMergeTaskRequest {
  string task_id = 1;
  CreateSegment merged_segment = 2;
  string rewrite_journal_relative_path = 3;
}

message CompleteSegmentMergeTaskResponse {
  oneof result {
    CompleteSegmentMergeTaskSuccess success = 1;
    CompleteSegmentMergeTaskError error = 2;
  }
}

message CompleteSegmentMergeTaskSuccess {}

message CompleteSegmentMergeTaskError {
  string message = 1;
}

message FailSegmentMergeTaskRequest {
  string task_id = 1;
  string error_message = 2;
}

message FailSegmentMergeTaskResponse {}

message ReleaseSegmentMergeTaskRequest {
  string task_id = 1;
  // Only release if this worker still holds the claim, so we never yank a task
  // that was already stale-reclaimed by another worker.
  string worker_id = 2;
}

message ReleaseSegmentMergeTaskResponse {}

SegmentDeletionService

Manages cleanup of tombstoned segments from storage.

RPCs

RPCDescription
GetTombstonedSegmentsForDeletionGet segments marked for deletion
ConfirmTombstoneDeletionConfirm segments have been deleted from storage
ConfirmRewriteJournalDeletion
GetOrphanRewriteJournals

Proto Definition

service SegmentDeletionService {
  rpc GetTombstonedSegmentsForDeletion(GetTombstonedSegmentsForDeletionRequest) returns (GetTombstonedSegmentsForDeletionResponse);
  rpc ConfirmTombstoneDeletion(ConfirmTombstoneDeletionRequest) returns (ConfirmTombstoneDeletionResponse);
  rpc ConfirmRewriteJournalDeletion(ConfirmRewriteJournalDeletionRequest) returns (ConfirmRewriteJournalDeletionResponse);
  rpc GetOrphanRewriteJournals(GetOrphanRewriteJournalsRequest) returns (GetOrphanRewriteJournalsResponse);
}

message GetTombstonedSegmentsForDeletionRequest {
  // Empty - service uses internal configuration
}

message GetTombstonedSegmentsForDeletionResponse {
  repeated TombstonedSegmentInfo segments = 1;
}

message TombstonedSegmentInfo {
  string segment_id = 1;
  string table_id = 2;
  CloudObjectKey cloud_object_key = 3;
  int64 tombstone_time_nanos = 4;
  int64 tombstone_version = 5;
  // Set when the segment is a rewrite output whose journal is still
  // registered; the journal dies with the segment it describes.
  CloudObjectKey rewrite_journal_key = 6;
}

message ConfirmTombstoneDeletionRequest {
  repeated string segment_ids = 1;
}

message ConfirmTombstoneDeletionResponse {
  oneof result {
    ConfirmTombstoneDeletionSuccess success = 1;
    ConfirmTombstoneDeletionError error = 2;
  }
}

message ConfirmTombstoneDeletionSuccess {
  int32 deleted_count = 1;
  repeated RewriteJournalReadyForDeletion journals_ready = 2;
}

message RewriteJournalReadyForDeletion {
  string output_segment_id = 1;
  CloudObjectKey journal_key = 2;
}

message ConfirmTombstoneDeletionError {
  string message = 1;
}

message ConfirmRewriteJournalDeletionRequest {
  repeated string output_segment_ids = 1;
}

message ConfirmRewriteJournalDeletionResponse {
  int32 cleared_count = 1;
}

message GetOrphanRewriteJournalsRequest {
  // Empty - service uses internal configuration
}

message GetOrphanRewriteJournalsResponse {
  repeated RewriteJournalReadyForDeletion journals = 1;
}

SegmentLookupService

Discovers segments matching a time range and table filter.

RPCs

RPCDescription
ResolveQueryCutThe query's cut — V_q plus the baby-lane routes — without touching the segment tables. One small query and an in-memory frontier read, where FindSegments also pages the segment list and resolves storage config per segment. The coordinator takes this first so it can start the nursery lane while the segment lookup is still in flight, then pins the same cut on FindSegments so the two lanes cannot disagree about where one stops and the other begins.
FindSegmentsFind segments by time range and table names (paginated)
GetSegmentsByIdsGet specific segments by their IDs
FilterCacheableSegmentPathsOf the given storage paths, which are still worth caching. The caller sets how long a tombstoned segment stays cacheable, so the retention policy lives with the cache that has to apply it, not here. Paths meta has never heard of are simply absent from the reply, so a caller can treat one negative answer as conclusive.

Proto Definition

service SegmentLookupService {
  // The query's cut — `V_q` plus the baby-lane routes — without touching the
  // segment tables. One small query and an in-memory frontier read, where
  // `FindSegments` also pages the segment list and resolves storage config per
  // segment. The coordinator takes this first so it can start the nursery lane
  // while the segment lookup is still in flight, then pins the same cut on
  // `FindSegments` so the two lanes cannot disagree about where one stops and
  // the other begins.
  rpc ResolveQueryCut(ResolveQueryCutRequest) returns (ResolveQueryCutResponse);
  rpc FindSegments(FindSegmentsRequest) returns (FindSegmentsResponse);
  rpc GetSegmentsByIds(GetSegmentsByIdsRequest) returns (GetSegmentsByIdsResponse);
  // Of the given storage paths, which are still worth caching. The caller sets
  // how long a tombstoned segment stays cacheable, so the retention policy
  // lives with the cache that has to apply it, not here. Paths meta has never
  // heard of are simply absent from the reply, so a caller can treat one
  // negative answer as conclusive.
  rpc FilterCacheableSegmentPaths(FilterCacheableSegmentPathsRequest) returns (FilterCacheableSegmentPathsResponse);
}

message ResolveQueryCutRequest {}

message ResolveQueryCutResponse {
  // `V_q`: the version cut clamped to what live nurseries have been handed,
  // or the latest registered version when no nursery holds a floor. Pass it
  // back on `FindSegmentsRequest.pinned_version_cut`.
  uint64 version_cut = 1;
  // Which nursery owns each stream group. Empty ⇒ no baby lane in play.
  repeated GroupRoute routes = 2;
}

message FindSegmentsRequest {
  TimeRange time_range = 1;
  repeated string table_names = 3;
  int32 limit = 4;
  optional string cursor = 5;
  // UUID of the database to resolve `table_names` against. Required;
  // server rejects empty. Callers (query service, admin tools) are
  // expected to have already resolved any name via `GetDatabase`.
  string database_id = 6;
  // Pin the lookup to a cut the caller already got from `ResolveQueryCut`,
  // instead of the server resolving a second one. Deriving `V_q` twice can
  // hand the cloud and nursery lanes different cuts, which opens a gap where
  // a segment is excluded by one and already dropped by the other. When set,
  // the response carries no routes — the caller already has them.
  optional uint64 pinned_version_cut = 7;
  // When set, only segments whose `ingest_time_range` also overlaps this.
  optional TimeRange ingest_time_range = 8;
}

message PagedResponse {
  optional string next_cursor = 1;
}

message FindSegmentsResponse {
  repeated SegmentInfo segments = 1;
  PagedResponse page = 2;
  // Snapshot of meta's monotonic segment-version counter
  // (`counters.version`) read in the same transaction as the
  // segment list. QC pins this as the watermark `V` for the query:
  // the cloud QWS lane sees only segments with `version <= V`; a
  // future nursery lane (Phase 4-5 of docs/dev/query-exec-in-nursery.md)
  // fills the gap with locally-staged segments that are either
  // unregistered or registered at `version > V`. The two sets are
  // disjoint by construction, so the merged result has no double-count.
  // When routes are present this is V_q (the version cut clamped to what
  // live nurseries have applied); otherwise the latest snapshot version.
  uint64 current_segment_version = 3;
  // Baby-lane routing: which nursery owns each stream group. Present ⇒ the
  // baby lane is in play and current_segment_version is V_q; absent ⇒ the
  // coordinator runs cloud-only. Populated by the meta-routing step.
  repeated GroupRoute routes = 4;
}

// One stream group and the nursery responsible for it.
message GroupRoute {
  string group_id = 1;
  // Durable owner node id; "" only if the DB owner is NULL (no live holder).
  string owner_nursery_id = 2;
}

message GetSegmentsByIdsRequest {
  repeated string segment_ids = 1;
}

message GetSegmentsByIdsResponse {
  repeated SegmentInfo segments = 1;
}

message FilterCacheableSegmentPathsRequest {
  // Paths relative to the bucket's configured base path, i.e. what
  // `segments.relative_storage_path` holds.
  repeated string relative_paths = 1;
  // How long a segment tombstoned as `replaced` (merge/rewrite) stays
  // cacheable. Its rows live on in the replacement, so the only reader left is
  // a query planned before the swap.
  int64 retain_after_replaced_seconds = 2;
  // How long a segment tombstoned for any other reason (retention, table
  // deleted) stays cacheable. Nothing succeeds it, and raising retention can
  // resurrect it, so this is normally the longer of the two.
  int64 retain_after_deleted_seconds = 3;
}

message FilterCacheableSegmentPathsResponse {
  // The subset of the request still worth caching. Unordered.
  repeated string cacheable_relative_paths = 1;
}

JanitorService

Provides cluster statistics and merge task management for maintenance operations.

RPCs

RPCDescription
GetJanitorStatsGet segment count, sizes, and outstanding merge tasks
ListTableStatsThe same segment aggregate as GetJanitorStats, grouped by table. One query instead of one GetJanitorStats call per table, which is what a caller wanting the whole breakdown had to do before.
GetSegmentSizeStatsGet segment size distribution by tier
ListMergeTasksList active merge tasks
GetMergeTaskGet detailed info about a specific merge task
UnclaimMergeTaskRelease a stale merge task claim
CreateMergeTasksCreate new merge tasks for eligible segments
RewriteAllSegmentsCreate one single-segment merge task per existing segment not already in a merge task. This forces every segment through the rewrite pipeline to pick up index fixes.
DeleteMergeTask
WriteOffSegmentWrite off one live segment whose data is lost or corrupt: tombstone it, drop the merge tasks that name it, and record the write-off in the table's audit chain under the caller's identity and reason. Only a caller the gateway authenticated can, as the chain names who did it. Its object is left for the segment deleter.

Proto Definition

service JanitorService {
  rpc GetJanitorStats(GetJanitorStatsRequest) returns (GetJanitorStatsResponse);
  // The same segment aggregate as GetJanitorStats, grouped by table. One query
  // instead of one GetJanitorStats call per table, which is what a caller
  // wanting the whole breakdown had to do before.
  rpc ListTableStats(ListTableStatsRequest) returns (ListTableStatsResponse);
  rpc GetSegmentSizeStats(GetSegmentSizeStatsRequest) returns (GetSegmentSizeStatsResponse);
  rpc ListMergeTasks(ListMergeTasksRequest) returns (ListMergeTasksResponse);
  rpc GetMergeTask(GetMergeTaskRequest) returns (GetMergeTaskResponse);
  rpc UnclaimMergeTask(UnclaimMergeTaskRequest) returns (UnclaimMergeTaskResponse);
  rpc CreateMergeTasks(CreateMergeTasksRequest) returns (CreateMergeTasksResponse);
  // Create one single-segment merge task per existing segment not already in a merge task.
  // This forces every segment through the rewrite pipeline to pick up index fixes.
  rpc RewriteAllSegments(RewriteAllSegmentsRequest) returns (RewriteAllSegmentsResponse);
  rpc DeleteMergeTask(DeleteMergeTaskRequest) returns (DeleteMergeTaskResponse);
  // Write off one live segment whose data is lost or corrupt: tombstone it, drop the merge
  // tasks that name it, and record the write-off in the table's audit chain under the
  // caller's identity and reason. Only a caller the gateway authenticated can, as the chain
  // names who did it. Its object is left for the segment deleter.
  rpc WriteOffSegment(WriteOffSegmentRequest) returns (WriteOffSegmentResponse);
}

message GetSegmentSizeStatsRequest {
  // Optional maximum compressed size in bytes (e.g., 10485760 for 10MB).
  // Only segments with compressed_size <= this value are included.
  optional int64 max_compressed_size = 1;
  // Optional minimum age in seconds. Only segments created after now - min_age are included.
  optional int64 min_age_secs = 2;
  // Optional maximum age in seconds. Only segments created before now - max_age are included.
  optional int64 max_age_secs = 3;
  // Optional: scope to a specific table by UUID. Pre-PR-4a this took a
  // name; switching to id avoids cross-database collisions when multi-DB
  // arrives.
  optional string table_id = 4;
  // Optional: scope to every table in this database, by UUID.
  optional string database_id = 5;
}

message SegmentSizeTier {
  int32 tier = 1;
  string tier_min = 2;
  string tier_max = 3;
  int64 segment_count = 4;
  int64 total_size = 5;
  int64 smallest = 6;
  int64 largest = 7;
  int64 avg_size = 8;
  string earliest_time = 9;
  string latest_time = 10;
}

message GetSegmentSizeStatsResponse {
  repeated SegmentSizeTier tiers = 1;
}

message GetJanitorStatsRequest {
  // Optional: scope to a specific table by UUID. Pre-PR-4a this took a
  // name; switching to id avoids cross-database collisions.
  optional string table_id = 1;
  // Optional: scope to every table in this database, by UUID.
  optional string database_id = 2;
}

message GetJanitorStatsResponse {
  int64 segment_count = 1;
  int64 outstanding_merge_tasks = 2;
  int64 total_size_bytes = 3;
  int64 total_compressed_size_bytes = 4;
  int64 segments_incl_tombstoned = 5;
}

message ListTableStatsRequest {
  // Optional: scope to a specific table by UUID — one row, the same answer
  // GetJanitorStats gives for that table.
  optional string table_id = 1;
  // Optional: scope to every table in this database, by UUID.
  optional string database_id = 2;
}

message TableSegmentStats {
  string table_id = 1;
  int64 segment_count = 2;
  int64 total_size_bytes = 3;
  int64 total_compressed_size_bytes = 4;
  int64 segments_incl_tombstoned = 5;
}

message ListTableStatsResponse {
  // One entry per table that has at least one segment row, tombstoned rows
  // included. A table with none is absent rather than present at zero: the
  // segments table is the only thing consulted, so this RPC cannot tell an
  // empty table from a table that does not exist.
  repeated TableSegmentStats tables = 1;
}

message ListMergeTasksRequest {
  // Optional: scope to a specific table by UUID. Pre-PR-4a this took a
  // name; switching to id avoids cross-database collisions.
  optional string table_id = 1;
  // Optional: scope to every table in this database, by UUID.
  optional string database_id = 2;
}

message ListMergeTasksResponse {
  repeated MergeTaskSummary tasks = 1;
}

message MergeTaskSummary {
  string task_id = 1;
  string table_id = 2;
  int32 segment_count = 3;
  string assigned_to_worker = 4;
  string created_at = 5;
  string assigned_at = 6;
  // How much data the task will move, summed over its input segments. A count of
  // segments does not say whether a task is a minute of work or an hour, which is
  // what separates a slow merge from a stuck one.
  int64 total_size_bytes = 7;
  int64 total_compressed_size_bytes = 8;
  // The task's table and its database, by name; empty once the table is gone.
  string table_name = 9;
  string database_name = 10;
}

message GetMergeTaskRequest {
  string task_id = 1;
}

message UnclaimMergeTaskRequest {
  string task_id = 1;
  // Minimum duration in seconds the task must have been claimed before it can be unclaimed.
  // Defaults to 3600 (1 hour) if not set.
  int64 min_claimed_secs = 2;
}

message UnclaimMergeTaskResponse {
  // True if the task was unclaimed, false if it was not found or not yet old enough.
  bool unclaimed = 1;
}

message CreateMergeTasksRequest {
  // Optional maximum compressed size in bytes. Only segments with compressed_size <= this value.
  optional int64 max_compressed_size = 1;
  // Optional minimum age in seconds. Only segments older than now - min_age.
  optional int64 min_age_secs = 2;
  // Optional maximum age in seconds. Only segments newer than now - max_age.
  optional int64 max_age_secs = 3;
  // Optional: scope to a specific table by UUID. Pre-PR-4a this took a
  // name; switching to id avoids cross-database collisions when multi-DB
  // arrives.
  optional string table_id = 4;
  // Optional: scope to every table in this database, by UUID.
  optional string database_id = 5;
}

message CreateMergeTasksResponse {
  // Number of merge tasks created
  int32 tasks_created = 1;
  // Total number of segments included across all tasks
  int64 segments_included = 2;
}

message GetMergeTaskResponse {
  string task_id = 1;
  string table_id = 2;
  int32 segment_count = 3;
  string assigned_to_worker = 4;
  string created_at = 5;
  int64 smallest_segment_size = 6;
  int64 largest_segment_size = 7;
  int64 smallest_segment_compressed_size = 8;
  int64 largest_segment_compressed_size = 9;
  string earliest_time = 10;
  string latest_time = 11;
  int64 total_size = 12;
  int64 total_compressed_size = 13;
  string assigned_at = 14;
  // The task's table and its database, by name; empty once the table is gone.
  string table_name = 15;
  string database_name = 16;
}

message RewriteAllSegmentsRequest {
  // Optional: scope to a specific table by UUID. Pre-PR-4a this took a
  // name; switching to id avoids cross-database collisions.
  optional string table_id = 1;
  // Optional: scope to every table in this database, by UUID.
  optional string database_id = 2;
}

message RewriteAllSegmentsResponse {
  // Number of single-segment merge tasks created
  int32 tasks_created = 1;
  // Total number of segments that will be rewritten
  int64 segments_included = 2;
}

message DeleteMergeTaskRequest {
  string task_id = 1;
}

message DeleteMergeTaskResponse {
  // True if the task was found and deleted.
  bool deleted = 1;
}

message WriteOffSegmentRequest {
  // The segment's table, by UUID; a segment of another table is refused.
  string table_id = 1;
  string segment_id = 2;
  // Why, recorded in the audit chain beside the caller's identity. Required.
  string reason = 3;
}

message WriteOffSegmentResponse {
  string segment_id = 1;
  string table_id = 2;
  // The segment's event-time range, in nanoseconds since the Unix epoch.
  int64 time_range_start_ns = 3;
  int64 time_range_end_ns = 4;
  int32 number_of_events = 5;
  // Sequence number of the chain_rebaselined event that records the write-off.
  int64 audit_seq = 6;
  // The reason as the audit chain records it, naming the caller.
  string audit_reason = 7;
  // How many pending merge tasks named the segment; all of them are removed.
  int64 merge_tasks_removed = 8;
}

VidxService

The vector-index write gate and the per-format segment inventory.

RPCs

RPCDescription
GetVidxWriteGateThe gate, its window, and every condition keeping writes from opening.
OpenVidxWritesOpens vector-index writes. Refused with FAILED_PRECONDITION, naming every unmet condition, unless the floor is at least 1, writes have been closed for at least one grant period, and the window has held on this meta. Opening an open gate changes nothing. Refused or not, it releases an operator's close, so a meta in AUTO mode opens writes once they could.
CloseVidxWritesCloses vector-index writes at once, and holds them closed: a meta in AUTO mode never reopens them by itself. Writers stop starting new-format builds when their grant expires. Closing a closed gate changes nothing but the hold.
ListVidxInventoryLive segments per table and vector-index format.

Proto Definition

// The vector-index write gate and the segment inventory. See
// docs/dev/vidx-index-implementation.md step 4.
service VidxService {
  // The gate, its window, and every condition keeping writes from opening.
  rpc GetVidxWriteGate(GetVidxWriteGateRequest) returns (VidxWriteGateStatus);

  // Opens vector-index writes. Refused with FAILED_PRECONDITION, naming every
  // unmet condition, unless the floor is at least 1, writes have been closed
  // for at least one grant period, and the window has held on this meta.
  // Opening an open gate changes nothing. Refused or not, it releases an
  // operator's close, so a meta in AUTO mode opens writes once they could.
  rpc OpenVidxWrites(OpenVidxWritesRequest) returns (VidxWriteGateStatus);

  // Closes vector-index writes at once, and holds them closed: a meta in AUTO
  // mode never reopens them by itself. Writers stop starting new-format
  // builds when their grant expires. Closing a closed gate changes nothing
  // but the hold.
  rpc CloseVidxWrites(CloseVidxWritesRequest) returns (VidxWriteGateStatus);

  // Live segments per table and vector-index format.
  rpc ListVidxInventory(ListVidxInventoryRequest) returns (ListVidxInventoryResponse);
}

message GetVidxWriteGateRequest {}

message OpenVidxWritesRequest {}

message CloseVidxWritesRequest {}

message VidxWriteGateStatus {
  bool open = 1;
  // Incremented on every open; 0 before the first.
  uint64 generation = 2;
  // Last open or close, nanoseconds since unix epoch; 0 if never changed.
  int64 changed_at_nanos = 3;
  // Whether this meta hands out grants: open, and the window has held here
  // since meta started, or it grants on its predecessor's verification of
  // the readers (granting_without_window).
  bool granting = 4;
  // Start of the current window, nanoseconds since unix epoch. May lie in the
  // future while a replica that breaks it is still inside its liveness window.
  int64 window_held_since_nanos = 5;
  int64 window_nanos = 6;
  int64 grant_period_nanos = 7;
  // Every condition that keeps writes from opening now, one sentence each.
  // Empty when OpenVidxWrites would succeed (or the gate is open and nothing
  // has lapsed).
  repeated string unmet = 8;
  uint32 vidx_floor = 9;
  // Current time on meta, nanoseconds since unix epoch.
  int64 now_nanos = 10;
  // This meta holds the gate-authority lock. Only the authority judges the
  // window, opens writes and grants; any meta closes them and refuses leases
  // below level 2 while they are open.
  bool authority = 11;
  // This meta's row in `instances`.
  string instance_id = 12;
  // Every meta seen recently, with the gate protocol it runs, oldest
  // registration first. A meta that predates the protocol registers nothing.
  repeated VidxMetaInstance instances = 13;
  // Who opens writes on this meta.
  VidxWritesMode writes_mode = 14;
  // When an operator closed writes, nanoseconds since unix epoch; 0 when no
  // operator's close holds them. While set, a meta in AUTO mode never opens
  // writes by itself; an operator's OpenVidxWrites clears it.
  int64 closed_by_operator_at_nanos = 15;
  // When a gate authority at floor 2 or more last confirmed every reader at
  // level 2 for a full window, nanoseconds since unix epoch; 0 when none
  // stands. A meta that takes the authority shortly after, while it was
  // already running, grants on it instead of waiting for its own window.
  int64 readers_verified_at_nanos = 16;
  // When any meta last accepted a reader below level 2, nanoseconds since
  // unix epoch; 0 if never. It withdraws the verification, which returns only
  // a full window later.
  int64 reader_below_level_seen_at_nanos = 17;
  // This meta grants on its predecessor's verification of the readers; its
  // own window has not held yet.
  bool granting_without_window = 18;
}

enum VidxWritesMode {
  // A meta that predates the mode.
  VIDX_WRITES_MODE_UNSPECIFIED = 0;
  // The gate authority opens writes itself the first time they could open,
  // unless an operator has closed them.
  VIDX_WRITES_MODE_AUTO = 1;
  // Only OpenVidxWrites opens them.
  VIDX_WRITES_MODE_MANUAL = 2;
}

message VidxMetaInstance {
  string instance_id = 1;
  int32 protocol_version = 2;
  int64 first_seen_nanos = 3;
  int64 last_seen_nanos = 4;
}

message ListVidxInventoryRequest {}

message VidxInventoryRow {
  string table_id = 1;
  string table_name = 2;
  string database_id = 3;
  VidxFormat format = 4;
  // V1 only: of `segments`, how many left a field unindexed for a transient
  // reason.
  int64 transient_unindexed = 5;
  int64 segments = 6;
  // Whether the table has vector fields configured.
  bool has_vector_fields = 7;
  // The table's database, by name: table names repeat across databases.
  string database_name = 8;
}

message ListVidxInventoryResponse {
  repeated VidxInventoryRow rows = 1;
}

Common Types

Shared message types used across multiple services.

// Both timestamps set or neither; server enforces delete_at >= tombstoned_at.
// Unix nanos (not google.protobuf.Timestamp) to dodge the well-known-types include path.
message Tombstone {
  int64 tombstoned_at_unix_ns = 1;
  int64 delete_at_unix_ns = 2;
}

message CloudBucket {
  string endpoint = 1;
  string bucket = 2;
  string region = 3;       // empty = provider default
  string path = 4;         // customer-configured base path within bucket (can be empty)
  string provider = 5;     // "s3" (default) or "gcs"
}

message CloudObjectPrefix {
  CloudBucket bucket = 1;
  string prefix = 2;       // e.g. "tables/ds-123/segments"
}

message CloudObjectKey {
  CloudObjectPrefix prefix = 1;
  string key = 2;           // e.g. "seg-456.ttseg"
}

message TimeRange {
  int64 start_time = 1; // in nanoseconds since the unix epoch
  int64 end_time = 2; // in nanoseconds since the unix epoch
}

message CreateSegment {
  string table_id = 1;
  TimeRange time_range = 2;
  int64 size_bytes = 3;
  int64 compressed_size_bytes = 4;
  int32 number_of_events = 5;
  CloudObjectKey cloud_object_key = 6;
  // AUDT digest of the segment file's audit chunk (32 bytes). Empty
  // for legacy segments without an AUDT chunk. New merger outputs
  // populate it.
  bytes audt_hash = 7;
  // Hash algorithm used for `audt_hash`. Wire codes match
  // libs/segment_files/src/audt.rs::AudtAlgo
  // (0 = sha256, 1 = blake3-256, 2 = sha3-256). Ignored when
  // `audt_hash` is empty.
  uint32 audt_algo = 8;
  // Ingest-time min/max across rows in the segment, taken from the
  // merger's verifier (MIMX on `ingest_time`). Drives ingest-time slicing
  // in the coordinator and the freeze watermark for the map-reduce-state
  // cache. The invariant `event_time <= ingest_time` is enforced at write.
  TimeRange ingest_time_range = 9;
  // Content/interpretation version of the segment's encoded contents (the high
  // u16 of the TSEG header `version` field). Bumped when a writer change requires
  // re-encoding existing segments without a layout change. Mirrored into
  // `segments.content_version`; segments below the current code constant are
  // auto-enqueued for rewrite. See bzrk_lib::SEGMENT_CONTENT_VERSION.
  uint32 content_version = 10;
  // The vector-index format the writer knows the segment carries, from its
  // manifest. Absent from a writer that predates the field, which meta
  // records as unknown.
  VidxManifestSummary vidx = 11;
}

// Which vector-index format a published segment carries. See
// docs/dev/vidx-index-implementation.md step 4.
enum VidxFormat {
  // Not reported.
  VIDX_FORMAT_UNKNOWN = 0;
  // No vector artifacts.
  VIDX_FORMAT_NONE = 1;
  // Today's artifacts: VCEN, VTPH, VTPC.
  VIDX_FORMAT_LEGACY = 2;
  // The vector index format, described by a VMAN manifest.
  VIDX_FORMAT_V1 = 3;
}

message VidxManifestSummary {
  VidxFormat format = 1;
  // V1 only: some field was left unindexed for a transient reason, such as an
  // embedding failure, so a rewrite may still index it.
  bool transient_unindexed = 2;
}

// Permission to start new-format vector-index field builds. The writer
// measures the validity from when it sent the request this grant answers, and
// honours only a grant of the newest generation it has seen.
message VidxWriteGrant {
  // Incremented on every open of the gate.
  uint64 generation = 1;
  uint64 validity_millis = 2;
}

message SegmentInfo {
  string id = 1;
  TimeRange time_range = 2;
  int64 size_bytes = 3;
  int64 compressed_size_bytes = 4;
  int32 number_of_events = 5;
  string table_id = 6;
  CloudObjectKey cloud_object_key = 7;
  // AUDT digest of the segment file's audit chunk (32 bytes). Empty
  // for pre-existing v0 rows that have NULL `audt_hash` in meta; a
  // query worker opening the segment uses this to verify the file
  // against tampering at load time when present.
  bytes audt_hash = 8;
  // Hash algorithm wire code for `audt_hash` (see AudtAlgo). Ignored
  // when `audt_hash` is empty.
  uint32 audt_algo = 9;
  // Ingest-time min/max across rows in the segment, populated from the
  // merger's verifier. Seeded from `time_range` for rows older than
  // migration 010 (safe under the `event_time <= ingest_time` invariant).
  TimeRange ingest_time_range = 10;
}

message ShardingField {
  string field_name = 1;
  double weight = 2;
}

// A message rather than a bare name so per-field options can be added later.
message VectorField {
  string field_name = 1;
}

message SegmentMergeTaskInfo {
  // Once `generation`.
  reserved 3;
  string task_id = 1;
  string table_id = 2;
  repeated SegmentInfo segments = 4;
  CloudObjectPrefix cloud_object_prefix = 5;
  repeated ShardingField sharding_fields = 6;
  // Fields the merge builds vector-search artifacts for. Empty means none
  // configured, and the janitor falls back to its environment.
  repeated VectorField vector_fields = 7;
}

message TableInfo {
  string id = 1;
  string name = 2;
  CloudObjectPrefix cloud_object_prefix = 3;
  // PR 5: which database this table belongs to. Carries the
  // `tables.database_id` column added by migration 026. Required for
  // nursery routing to scope name lookups by database.
  string database_id = 4;
  // Shard set the nursery's baby-merge keys its SHAR index on. Empty means
  // none configured, as with SegmentMergeTaskInfo.sharding_fields.
  repeated ShardingField sharding_fields = 5;
  // Fields the nursery builds vector-search artifacts for. Empty means none
  // configured, and the nursery falls back to its environment.
  repeated VectorField vector_fields = 6;
}

// One labelled count in a bounded, reason-keyed breakdown.
message ReasonCount {
  string reason = 1;
  uint64 count = 2;
}

On this page