Skip to main content

KV Store

Fast namespaced key-value storage for persisting small values across agent runs. The KV Store is SQLite-backed with WAL mode for concurrent access, instance-based (each instance is a logical container with its own namespaces), and delivers real-time updates via WebSocket.

KV Store


Storage Layout​

ItemLocation
Database{projectPath}/.codebolt/db/project.db (shared with VectorDB and EventLog)
Instance metadata{projectPath}/.codebolt/kvstore/instances/{instanceId}.json

Table Schema — kv_store​

ColumnTypeDescription
instance_idTEXTUUID of the owning instance
namespaceTEXTLogical namespace within the instance
keyTEXTRecord key
valueTEXTJSON-stringified value
updated_atINTEGEREpoch milliseconds of last write

Primary key: (instance_id, namespace, key) — conflicts trigger an upsert.

SQLite PRAGMAs​

The database is configured with:

  • WAL mode for concurrent reads and writes
  • Appropriate synchronous level for durability
  • TEMP_STORE MEMORY to keep temporary tables in RAM

Instance Management​

Each instance is a logical container described by the following metadata:

FieldDescription
idUUID
nameHuman-readable name
descriptionOptional description
recordCountLive count of records (computed from DB on read)
namespacesLive list of namespaces (computed from DB on read)
createdAtCreation timestamp
updatedAtLast-modified timestamp

Metadata is stored as individual JSON files and sorted by createdAt DESC when listed.

ensureInstance(name)​

Uses a get-or-create pattern: looks up an existing instance by name, and creates a new one if no match is found. This is the mechanism behind the autoCreateInstance flag on the set endpoint.


Data Operations​

set(instanceId, namespace, key, value)​

Upserts a record into kv_store. The value is JSON.stringify-ed before storage. Emits the kvStore:record-set WebSocket event on success.

get(instanceId, namespace, key)​

Retrieves a record and JSON.parses its value. Returns null if the key does not exist.

delete(instanceId, namespace, key)​

Deletes a single record. Emits kvStore:record-deleted. Returns a boolean indicating whether a row was removed.

deleteNamespace(instanceId, namespace)​

Deletes all records in the given namespace. Emits kvStore:namespace-cleared. Returns the count of deleted records.

getNamespaces(instanceId)​

Returns a sorted array of distinct namespace values for the instance.

getRecordCount(instanceId, namespace?)​

Counts records belonging to an instance, optionally filtered to a single namespace.


Query DSL​

The KVQueryDSL allows structured queries against the store:

interface KVQueryDSL {
instance: string; // instance ID
namespace: string; // namespace to query
where?: KVFilter[]; // filter conditions
order?: KVQueryOrder; // sorting
limit?: number; // max results
}

Filter Operators (KVFilterOperator)​

OperatorDescription
=Exact match
!=Not equal
starts_withKey prefix match
containsSubstring match
inValue is in a provided array
existsKey exists (no value argument needed)

Allowed filter fields: key, updated_at

Query Compilation​

KVQueryDSL is compiled to parameterized SQL via kvStoreQueryCompiler. The compiler translates the filter array and ordering into WHERE clauses with bound parameters, preventing SQL injection.

Constraints​

ConstraintValue
MAX_FILTERS5
MAX_QUERY_LIMIT500
DEFAULT_QUERY_LIMIT100

Database Migrations​

Two migrations are applied in order:

  1. CreateKVStoreTable — creates the kv_store table with columns instance_id, namespace, key, value, and updated_at.
  2. CreateKVMigrationsTable — creates the migration-tracking table used to record which migrations have run.

WebSocket Events​

All mutations emit events so that connected clients (e.g., the UI panel) can update in real time.

EventPayload
kvStore:instance-createdInstance metadata
kvStore:instance-updatedInstance metadata
kvStore:instance-deletedInstance ID
kvStore:record-set{ instanceId, namespace, key, value }
kvStore:record-deleted{ instanceId, namespace, key }
kvStore:namespace-cleared{ instanceId, namespace }

REST API​

Instance Endpoints​

MethodPathDescription
POST/kvstore/instancesCreate a new instance
GET/kvstore/instancesList all instances
GET/kvstore/instances/:idGet instance with live stats (recordCount, namespaces)
PUT/kvstore/instances/:idUpdate instance name or description
DELETE/kvstore/instances/:idDelete instance and all its records

Record Endpoints​

MethodPathDescription
POST/kvstore/kvSet a value. Body: { instanceId, namespace, key, value, autoCreateInstance }
GET/kvstore/kv/:instanceId/:namespace/:keyGet a value
DELETE/kvstore/kv/:instanceId/:namespace/:keyDelete a value
DELETE/kvstore/namespace/:instanceId/:namespaceDelete all records in a namespace

Query Endpoint​

MethodPathDescription
POST/kvstore/queryExecute a KVQueryDSL query

Note: The autoCreateInstance flag on the set endpoint triggers ensureInstance — if the instance name does not exist yet, it is created automatically before the value is written.


SDK Reference​

Create an Instance​

const instance = await codebolt.kvstore.createInstance({
name: "my-agent-state",
description: "Persisted state for my agent"
});

Set a Value​

await codebolt.kvstore.set(instance.id, "config", "theme", "dark");

Get a Value​

const theme = await codebolt.kvstore.get(instance.id, "config", "theme");
// "dark"

Delete a Value​

await codebolt.kvstore.delete(instance.id, "config", "theme");

Delete a Namespace​

const deletedCount = await codebolt.kvstore.deleteNamespace(instance.id, "config");

Query Records​

const results = await codebolt.kvstore.query({
instance: instance.id,
namespace: "user-prefs",
where: [
{ field: "key", operator: "starts_with", value: "ui." }
],
order: { field: "updated_at", direction: "desc" },
limit: 50
});

Get Namespaces​

const namespaces = await codebolt.kvstore.getNamespaces(instance.id);
// ["config", "user-prefs"]

Get Record Count​

const total = await codebolt.kvstore.getRecordCount(instance.id);
const nsCount = await codebolt.kvstore.getRecordCount(instance.id, "config");