Consume relation changes with a lightweight Python client
Source SDK preview; not yet merged or published to PyPI.
VerifiedRelationConsumer uses the typed relation feed
available in server 0.13.0. It validates a complete bounded page before delivering
any event, applies effects through a caller-owned durable sink, and advances an
owned checkpoint only after the sink retains the corresponding witness.
The client uses the Python standard library. It generates no embeddings, runs no model, and embeds no QilbeeDB engine. The SQLite example is an optional destination adapter. An existing Python package installation does not establish that this source-preview client is available.
Configure a stream-specific destination#
The credential needs memory_read and memory_checkpoint in the exact scope.
Supply the expected company and subject explicitly. Each operation pins one
credential and checks its current identity before accessing the stream. A supplier
can return a rotated key on the next operation, with the same company and subject.
import os
from qilbeedb import VerifiedRelationConsumer
consumer = VerifiedRelationConsumer(
"https://api.example.com",
lambda: os.environ["QILBEE_API_KEY"],
tenant_id="company",
subject_id="graph-cache-service",
scope={
"project_id": "project",
"agent_id": "agent",
"mission_id": None,
"visibility": "private",
},
consumer_id="context-graph-cache",
page_size=100,
timeout=10.0,
max_response_bytes=4 * 1024 * 1024,
)
page_size is 1–256. TLS verification, response byte limits, redirect rejection,
exact integer decoding, duplicate-field rejection and failure classification use
the same transport as the memory client.
Credentials are not persisted, proxy environment variables are ignored, and no
request is retried automatically. The timeout bounds individual socket operations;
it is not a total call deadline.
A relation binding includes the stream, company, subject, exact scope and consumer name. It differs from the memory client's binding even if all configured names match. Server relocation or credential rotation does not change the binding. Independent database lineages need separate sink namespaces; a binding is not a server identity. Complete cursors and server diagnostics detect incompatible history before incremental processing.
Implement the durable boundary#
| Object or method | Contract |
|---|---|
RelationCursor |
Complete immutable version, stream, journal_id, sequence, and prefix_digest; use from_dict() and to_dict() for exact serialization |
RelationDelivery |
Binding, stable delivery ID, cursor, and historical relation-change metadata |
load_witness(binding_id) |
Return the complete relation cursor for the highest contiguous prefix represented durably by this destination, or None before reconciliation |
apply(delivery) |
Commit idempotent effects, delivery deduplication, and the witness together before returning |
The client rejects memory cursors, nonconsecutive deliveries, inconsistent scope, unsupported event kinds, invalid endpoint references and malformed digests. Python integers preserve all unsigned 64-bit revision and sequence values. Digests are opaque history witnesses verified by the server; the SDK does not independently reconstruct the journal or claim to verify cryptographic signatures.
Commit the destination transaction before returning from apply. A callback that
fails after committing may leave durable effects; resumption uses that witness
instead of replaying already witnessed events. A callback that fails to retain
its cursor is rejected. Callbacks must be synchronous.
The SDK prevents reentry on one instance, but provides no distributed lease. Coordinate independent sink writers and deduplicate inside their shared durable boundary. Checkpoint comparison prevents a losing writer from replacing remote progress, but cannot undo duplicate calls to an external service. Exactly-once external effects require the destination's own idempotency and reconciliation.
Initialize after explicit reconciliation#
diagnose() observes the journal, current checkpoint and optional witness without
altering them. An inactive stream does not prove there are no preexisting
relations. A writer can explicitly
activate the feed, or a
new relation mutation activates it atomically.
For a new cache, invalidate or reconcile its existing authorized contents and
durably retain a verified relation cursor before calling initialize. Include
memory changes in this strategy. Neither feed represents an atomic snapshot across
both streams. Never seed the sink by copying a server checkpoint whose effects
the destination cannot demonstrate.
from qilbeedb import RelationCursor
observation = consumer.diagnose()
if not observation["active"]:
raise RuntimeError("Explicit journal activation and reconciliation are required")
cursor = RelationCursor.from_dict(observation["high_watermark"])
# Reconcile or invalidate the destination's graph cache and durably retain
# this exact cursor for consumer.binding_id. The application supplies `sink`.
# This comment is not a reconciliation step.
checkpoint = consumer.initialize(sink, cursor)
Initialization requires the exact existing sink witness and a missing server
checkpoint. It never replaces progress, rewinds a witness, or silently recovers
incompatible history. If a checkpoint already exists, resume through
consume_once after checking destination state.
Catch up with bounded work#
fence = None
for _ in range(32): # Application-owned work and cancellation budget.
result = consumer.consume_once(sink, through=fence)
fence = result.high_watermark
if result.complete:
break
Each call delivers at most one validated page. Retaining high_watermark as
through fixes a finite catch-up fence; later commits remain for the next cycle.
Retain that fence if the work budget ends before completion. Clear it only when
starting a new cycle. A checkpoint behind the sink witness can be advanced without
redelivering durable effects. A checkpoint ahead of the witness stops processing.
RelationConsumptionResult contains the delivery count, page cursor, high
watermark, completeness, freshly observed checkpoint, and optional historical
receipt. The current checkpoint can be newer than the page when another
coordinated worker advances it. A successful write is followed by current-state
diagnostics; an old receipt never substitutes for current progress.
Queue invalidations safely with SQLite#
sdks/python/examples/verified_relation_sink.py provides
SQLiteRelationInvalidationSink. Run source examples with
PYTHONPATH=sdks/python:sdks/python/examples. It stores no memory bodies, vectors
or credentials. A synchronous=FULL SQLite WAL transaction records each delivery,
advances the exact witness and marks the binding's graph cache dirty.
After explicit initial reconciliation, call
seed_after_reconciliation(binding_id, cursor) once. Existing witnesses cannot be
overwritten. pending_invalidation(binding_id) returns the latest dirty cursor.
After durably invalidating that cache, call
acknowledge_invalidation(binding_id, observed_cursor). It clears only that exact
observation; a concurrently delivered newer event remains pending. Clearing an
invalidation never rewinds the sink witness or server checkpoint.
The example invalidates a whole binding. A selective destination needs a complete reverse dependency map, including multi-hop cached results. Invalidating only an assertion's two endpoint records can leave derived graph contexts stale. External cache invalidation and queue acknowledgement are not a distributed transaction: apply idempotent invalidation before acknowledging.
Preserve current context across both streams#
A relation event records a historical assertion operation, not current truth or permission. Consume the memory feed as a separate binding, too. Updating, deleting or rejecting a source can invalidate relations without a relation event. Time-based expiry can occur without either stream emitting an event.
Before reusing graph context, read current authorized typed graphs and/or relation eligibility, verify exact endpoint revisions, and honor coverage and validity limits. On servers supporting the 0.14.0 additional context extension, include declared evidence sources and their transitive ancestors in cache dependencies. A relation change event does not contain that full dependency set. Use current relation reads, or conservatively invalidate the scoped cache after a memory change. Treat 401/403, missing relations, changed revisions and expiry as reasons to invalidate or reconcile. Receiving all events does not grant a freshness lease. This SDK acknowledges notifications; it does not materialize a current knowledge snapshot.
Resolve failures without overwriting progress#
Shared ConsumerError types expose code, HTTP status where available, and
checkpoint_outcome: not_attempted, rejected, unknown, or acknowledged.
These outcomes describe the checkpoint attempt, not whether sink effects happened.
| Condition | Response |
|---|---|
| Missing witness or checkpoint | Reconcile explicitly; do not infer completed effects |
| HTTP 409 comparison failure | Read current state; preserve the original sink witness |
| Lost response or malformed receipt | Treat the checkpoint outcome as unknown; resume by diagnosis rather than blind write retry |
| Failure after a valid receipt | The write was acknowledged, but current progress remains unverified |
| Divergent history or checkpoint ahead of the sink | Stop incremental effects and reconcile with retained evidence |
| Revocation, lost scope or identity mismatch | Resolve current authority; do not substitute another subject |
| Sink failure or malformed page | Stop; retain the last durable state and inspect the failure |
The SDK performs no reconcile checkpoint operation. Intentional history
replacement uses the server's evidence-bearing recovery contract after the
application reconciles its real effects. A jointly restored database and sink
cannot prove effects that both restored copies forgot. These guarantees concern
consumption integrity, not improved retrieval relevance or agent reasoning.