DEV Community

Daniel Romitelli
Daniel Romitelli

Posted on • Originally published at craftedbydaniel.com

A Context Object Should Carry Its Receipt

A stored fact can be wrong in a quiet way. The answer still reads clean. A preference from an old exchange gets reused, the message goes out with confidence, and later nobody can tell why that detail was allowed back into the result.

That is the failure I built around. When a system returns remembered material, the caller needs the text plus the reason it passed the reuse check. A log line found after the action is weak evidence. The object that leaves the memory service has to carry the admission record with it.

1. Keep the outside surface small

This is the pattern I used in Holographic, Law-Bound Memory (HLM), a stand-alone memory brain outside application code. The README describes public Application Programming Interface (API) routes under /api/brain/*, with internal /api/v1/* services behind that layer.

The outside shape is intentionally thin: register an agent, write a fact, build a capsule. The Python Software Development Kit (SDK) in sdks/python/hlm_sdk/client.py shows the boundary without exposing table names or policy code:

import httpx

class HLMClient:
 def __init__(self, base_url: str, token: str | None = None):
 self.base_url = base_url.rstrip("/")
 self._client = httpx.AsyncClient(headers={"Authorization": f"Bearer {token}"} if token else None)

 async def register_agent(self, name: str):
 r = await self._client.post(f"{self.base_url}/api/brain/agents/register", json={"name": name})
 r.raise_for_status
 return r.json

 async def write_fact(self, text: str, tags: list[str] | None = None, selectors: list[str] | None = None):
 r = await self._client.post(f"{self.base_url}/api/brain/memory/facts",
 json={"text": text, "tags": tags or [], "selectors": selectors or []})
 r.raise_for_status
 return r.json

 async def build_capsule(self, query: str, budget_tokens: int = 2048):
 r = await self._client.post(f"{self.base_url}/api/brain/context/capsule",
 json={"query": query, "budget_tokens": budget_tokens})
 r.raise_for_status
 return r.json
Enter fullscreen mode Exit fullscreen mode

The TypeScript client in sdks/node/src/index.ts exposes the same calls as registerAgent, writeFact, and buildCapsule. That costs me a compatibility surface at the gateway. I accept the cost because admission policy in every consumer becomes drift. One caller skips a selector, another copies an old threshold, a third treats a nearby match as enough. Centralizing the decision gives the service a place to say yes or rebuild before the application acts.

2. Write facts with handles the service can check

A plain text memory is easy to save. It gives retrieval very little to inspect later. HLM writes each fact with tags, selectors, and an optional tenant field so the service has decision axes before a query shows up.

The write model in services/memory/app/main.py is small:

from typing import List
from pydantic import BaseModel, Field

class FactIn(BaseModel):
 text: str
 tags: List[str] = Field(default_factory=list)
 selectors: List[str] = Field(default_factory=list)
 tenant_id: str | None = None
Enter fullscreen mode Exit fullscreen mode

That class feeds create_fact. The row itself lands in brain_facts. Two more writes follow against the same fact id, one for facets and one for predicates. The response returns the new id with both sets attached.

generate_facets has two paths in the current scaffold. A known selector value produces a specific facet row. Anything empty or unmatched falls back to a general facet, built from the first 256 characters of the text with the token count capped at 64. Those are constants in the code rather than performance claims. What they show is the shape: retrieval sees more than a blob of prose.

generate_predicates turns selector strings into one predicate joined with AND, swapping the first colon for =. That is rough. It is also enough, because the fact now leaves the write path carrying handles a machine can check. The tradeoff lands on the writer. A caller that sends empty selectors can still store text, but later selection has fewer axes to test.

3. Decide admission before capsule assembly

The reuse service is Conformal-Causal Reuse (CCR). Its request model in services/ccr/app/main.py carries the cache key, artifact type, selectors, and optional numeric controls:

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI(title="HLM CCR", version="0.1.0")

class CCRRequest(BaseModel):
 tenant_id: str | None = None
 cache_key: str
 artifact_type: str = "resume_kit"
 selectors: list[str] = Field(default_factory=list)
 similarity: float | None = None
 tau: float | None = None
Enter fullscreen mode Exit fullscreen mode

The rule in reuse_or_rebuild is direct: a hit requires similarity > tau and the required selector kinds stakeholder, time, and channel to be present. Missing one of those kinds makes the service return rebuild. When the request omits values, the code uses 0.9 for similarity and 0.8 for tau. Those numbers are defaults in the function. They are not measured latency, quality, or production calibration.

The response includes decision, tau, similarity, and causal_ok. All four travel with the answer. Accepted material can name the rule that admitted it, and a rejection arrives as a rebuild decision instead of a silent empty match.

Calibration stays beside the same service. CalibIn accepts selector, similarity, and span_error; update_calibration computes a rounded tau and clamps it between 0.5 and 0.95. I kept that logic near the decision endpoint because threshold repair separated from the admission rule becomes another place for drift. The cost is coupling. CCR owns both the current decision and the local adjustment path.

4. Build the capsule with provenance attached

The orchestrator joins the pieces. In services/orchestrator/app/main.py, /api/v1/capsule derives selectors from the query and posts them to CCR. What comes back shapes the capsule: content, confidence values, reasoning metadata, and a proof value.

The intended object is signed context rather than an anonymous bag of nearest neighbors. The current branch is still a scaffold. Episode writes return receipt: "merkle:demo", and the orchestrator repeats that same demo value in its local capsule response. The slot is real and the hardening is unfinished, so the caveat stays in the design.

The four-step path is the engineering pattern:

flowchart TD
 factWrite[Fact write] --> governedReuse[Reuse decision]
 governedReuse --> capsuleBuild[Capsule build]
 capsuleBuild --> proofReceipt[Proof receipt]
Enter fullscreen mode Exit fullscreen mode

services/gateway/app/main.py contains merkle_root(items). It hashes the item strings and folds pairs until one hash is left, duplicating the last leaf when the count is odd. The result gets a merkle: prefix. The gateway is the right home for it, because the external object is formed there. Downstream code should receive a single object holding the selected material, the CCR decision fields, and a provenance value it can store or compare later.

The architecture document names this Proof-of-Context (PoC): Merkle roots over snapshot, version, tau, model, and ids. The label matters less than the placement. If applications learn to consume loose context first, provenance turns into a retrofit. Retrofitted evidence is usually optional. Optional evidence disappears under deadline pressure.

5. Preserve the same object across streamed updates

Memory work does not always end at the first capsule. services/orchestrator/app/main.py has a local loop that yields Server-Sent Events (SSE) through a StreamingResponse; each packet includes a generated packet_id, summary fields, next actions, and a timestamp before the loop pauses. The gateway forwards this through /api/brain/context/stream. The README and architecture notes describe the larger outbox path with leases, backoff, a dead-letter queue (DLQ), and a resume stream over SSE or WebSockets (WS). The current code handles the visible stream contract; the documented shape says lineage has to move with later packets as well. That adds overhead compared with returning an array from a nearest-neighbor endpoint, but a resumed update without the original admission data is just another loose event.

6. Own the memory lifecycle

This design buys safer reuse by moving work into the memory service. Writers must send useful selectors. The gateway becomes stricter. The service has to keep admission metadata and provenance beside the content from write, through CCR, into capsule creation and streaming. I prefer that pressure inside HLM over spreading half-copied rules through applications, because systems that remember should expose the conditions under which memory became usable.


๐ŸŽง Listen to the audiobook โ€” Spotify ยท Google Play ยท All platforms
๐ŸŽฌ Watch the visual overviews on YouTube
๐Ÿ“– Read the full 13-part series

Top comments (0)