Steve HutchinsonBig Pines
·4 min read·Scaling the Cognitive Substrate

Operating the Substrate in Production

Building the system is one thing. Running it 24/7 under real load is another. What production operations actually requires - monitoring memory quality, handling schema migrations, and the operational surprises that only appear in the real world.

Building a system is one thing. Running it in production, day after day, is something else entirely.

The Monitoring Problem

Standard service monitoring tells you whether the system is up and whether it is responding quickly. That is necessary but not sufficient for a memory system.

You also need to observe memory quality - and that requires custom instrumentation:

Consolidation lag - how far behind is the consolidation worker from the current event watermark? A growing lag means raw experiences are accumulating faster than they are being processed.

Trust score distribution - are trust scores distributing across the full range, or clustering at the top (everything looks high quality) or bottom (the feedback loop is not working)?

Retrieval quality stability - is the average trust score of retrieved memories stable over time, or slowly declining as the memory store grows?

Memory tier distribution - are memories moving between tiers as expected, or is everything stacking up in one tier?

Every service in the Cognitive Substrate calls initTelemetry() at startup to register traces, metrics, and OTLP export. The OTel setup is environment-driven and includes a clean shutdown path for flushing pending spans:

// packages/telemetry-otel/src/bootstrap.ts
//
// Call initTelemetry() once at startup - before any imports that use the OTel API.
// Config from environment:
//   OTEL_SERVICE_NAME            - required
//   OTEL_EXPORTER_OTLP_ENDPOINT  - optional (e.g. http://collector:4318)
//   OTEL_SDK_DISABLED            - "true" to disable in unit tests

export async function initTelemetry(config: TelemetryConfig): Promise<() => Promise<void>> {
  if (config.disabled) {
    return async () => undefined
  }

  const resource = new Resource({
    [SEMRESATTRS_SERVICE_NAME]: config.serviceName,
    [SEMRESATTRS_SERVICE_VERSION]: config.serviceVersion ?? '0.0.0',
  })

  const traceExporter = config.otlpEndpoint
    ? new OTLPTraceExporter({ url: `${config.otlpEndpoint}/v1/traces` })
    : new OTLPTraceExporter()

  _sdk = new NodeSDK({ resource, traceExporter })
  _sdk.start()

  // Return a shutdown function - caller must invoke on SIGTERM to flush spans.
  return async () => {
    await _sdk?.shutdown()
  }
}

// Each service builds its config from env vars:
export function telemetryConfigFromEnv(defaultServiceName: string): TelemetryConfig {
  return {
    serviceName: process.env['OTEL_SERVICE_NAME'] ?? defaultServiceName,
    serviceVersion: process.env['npm_package_version'] ?? '0.0.0',
    disabled: process.env['OTEL_SDK_DISABLED'] === 'true',
    otlpEndpoint: process.env['OTEL_EXPORTER_OTLP_ENDPOINT'],
  }
}

W3C traceparent headers are injected into every Kafka message by CognitiveProducer and extracted by CognitiveConsumer. This means distributed traces span the full pipeline - from telemetry ingestion through consolidation to retrieval - without any manual instrumentation in the worker code.

Schema Evolution

The ExperienceEvent schema has changed multiple times as I learned more about what data matters. Each schema change requires careful handling: new documents use the new format, old documents use the old format, and retrieval needs to work correctly for both during the migration window.

The solution is versioned document formats in OpenSearch and a retrieval layer that normalizes between versions at query time. Schema migrations run as background jobs that gradually rewrite old documents to the current format.

The Embedding Model Problem

The most expensive operational surprise: embedding model drift.

When I updated the embedding model used during ingestion, historical memories became semantically incompatible with new queries. The vector spaces of the old and new models are not aligned - a query embedded with the new model does not retrieve memories embedded with the old model reliably.

Fixing this requires re-embedding the entire historical corpus with the new model. That is a significant compute job that requires careful coordination with the live retrieval path to avoid serving degraded results during the transition.

The lesson: treat the choice of embedding model as a high-stakes long-term commitment, not an implementation detail.

What Production Actually Requires

Having a working system is great. Having a system you can trust in production - with alerts that fire when something actually matters, migrations that can run without downtime, and enough observability to diagnose problems without guessing - is the real engineering goal.

This is the final piece of infrastructure that turns the Cognitive Substrate from a promising project into something you can actually depend on.

Next up from memory

Ranked from series and tags, warmed by what the substrate is keeping salient across readers.

Accept to save reading progress, unlock continue-where-you-left-off, and allow a hosting-support ad at the end of posts. Anonymous session signals still help the live memory panels; we never publish reader IDs.