Skip to content

Observability rollout and operator guide

This guide covers the implemented Memorizz roadmap: native trace indexes, selection explanations, coverage, secure operator workflows and rollout gates. It does not configure, repair or deploy any host application. In particular, no OpenSpeech integration or production data change is part of this work.

Passing repository tests is not an end-to-end incident all-clear. The independent 5 September audit identified additional semantic, legacy-scope and operator-UI defects; the remediation is covered by tests/unit/test_observability_audit_remediation.py and both browser gates below. Host source-to-artifact and parser/browser evidence, configured lookup hooks, the deployed release identity and production rollout checks remain separate acceptance requirements. Missing historical evidence cannot be manufactured.

Reading evidence without overclaiming coverage

Analysis, health and inspector responses distinguish three independent facts:

Field Meaning
read_completeness Whether the selected stored-event window was loaded without truncation or normalization/query errors; never a guarantee that unwritten events exist
instrumentation_coverage unknown without declared profiles, partial when required stages are missing, or complete when all selected task scopes declare profiles whose stages were observed
outcome_verification recorded when verified outcome evidence is present, otherwise unknown; recorded evidence may describe failure and does not itself establish end-to-end success

An uninstrumented legacy trace displays “All stored events loaded; end-to-end coverage unknown.” Absent metrics use value: null and state: no_recorded_evidence for a complete read, or state: unknown for an incomplete/untrusted read. Summary numeric counts remain recorded-event counts, not assertions that no artifact was created or no browser delivery occurred. The low-level paginated query coverage field remains a read-page compatibility alias; never use it as a task-success signal. Event exports include evidence_coverage with the separate semantics.

Detail, analysis and health use the same authorized agent-or-registered-memory selection, conjoined with tenant/thread scope. Their combined store selection is conversation, tool_log, trace; /traces/events.json defaults explicitly to trace only. To export the combined bounded snapshot, use store=all with the same agent/thread/root/turn/task/thread-memory selection, including run and child-event time bounds. Combined export is metadata-only, up to 1,000 normalized events from 500 source rows per store; it is not a historical pagination API. Use the per-store cursor API for longer histories. Per-store exports reject task/thread-memory filters they cannot apply; they do not silently broaden those requests. Failure to load registered agent-memory associations returns HTTP 503, not a falsely complete narrower selection.

Narrowed root/turn counts use distinct recorded source provenance. Parent query counts and deduplication are exposed separately as parent_query_metadata; unknown selection-local deduplication stays null. A source row may contribute to multiple selections, so selected-root counts are not necessarily additive. Comparison applies its 1,000-event cap after selecting each root independently; unrelated roots cannot consume the selected root's event budget. The parent source-row window is still bounded and its truncation remains visible.

Finder results group events by root/task and preserve thread/root/turn/task/event identity in navigation. The selected event is highlighted; health/comparison links preserve the selection. The agent/thread overview is collapsed and bounded when an incident is selected. Empty lineage/artifact/contract panels explain the missing fields and host recording boundaries, and memory-only traces retain aggregate evidence without inventing source bindings.

GET /traces/capabilities.json reports permission-and-hook availability. The UI disables account resolution, current-artifact lookup and replay controls when their identity_resolver, artifact_resolver or resource_authorizer callback is absent. It does not install callbacks or connect to an application database. The existing audited, tenant-aware create_app(...) hook contract still applies. Legacy replay source arrays are decoded and type-checked centrally; [], JSON-encoded "[]" and null generate no references. Malformed arrays are rejected before resource authorization, never interpreted as characters. The same parser protects direct diagnostic/lineage calls; malformed raw list representations raise ValueError. Stored malformed source/task metadata is reported as untrusted normalization evidence.

Reveal, inspection and inert replay preserve thread, turn, task, run, memory and time selection. A reused event ID must resolve to exactly one event for reveal; otherwise the endpoint returns HTTP 409 and requests a narrower turn/run. Replay freezes the selected evidence window, not necessarily the entire root. Unknown earlier pages, untrusted metadata and non-advancing/duplicate pagination cannot become a complete replay window just because the last page is complete. Incomplete replay windows fail before invoking a host resource authorizer.

Native storage and rollout

Canonical source envelopes remain private shared-memory trace_bundle v2 records containing legacy or typed v3 children. New Memorizz trace writes are immutable: the first writer owns a tenant-scoped event/turn ID. An external-ID retry preserves the original timestamp and evidence. A changed observation, artifact revision or new attempt requires a new external ID/turn. Recommendations and experiment drafts retain their existing mutable lifecycle.

Provider Native storage Provisioning
Filesystem _observability/index.sqlite3, outside semantic recall Explicit initialize(), WAL, private directory/database permissions
MongoDB Dedicated observability_spans, observability_bundles, observability_previews, observability_state collections Explicit compound indexes for time, tenant, agent/thread, root, kind/status, resource, memory, job and error
Oracle Dedicated obs_spans, obs_bundles, obs_resources, obs_previews, obs_state tables Additive 008_observability.sql, schema-qualified binds/CLOBs, explicit initialization
Other providers Existing bundle compatibility queries No automatic native-index support is claimed

Reads never provision tables or modify connection/session schema. The Oracle migration ignores only “object already exists”; other DDL failures propagate. Use a migration identity with DDL permission, then a restricted runtime identity. SQLite/Oracle writes are transactional and serialized; Mongo event replacements are atomic with a durable pending checkpoint until all children are written. Pending/failed index writes make coverage untrusted. Canonical source writes survive index failure; inspect process counters and backfill to recover.

from memorizz.observability import ObservabilityMaintenance

# provider is the explicitly selected Memorizz provider, not an application DB.
maintenance = ObservabilityMaintenance(provider)
maintenance.initialize()  # deliberate, additive provisioning

# Review one page before making index writes.
print(maintenance.backfill(since="2026-09-01T00:00:00Z"))  # dry run
cursor = None
while True:
    result = maintenance.backfill(
        since="2026-09-01T00:00:00Z", limit=100,
        cursor=cursor, dry_run=False,
    )
    if result["failed"]:
        raise RuntimeError("Backfill failed; inspect the content-free error codes")
    cursor = result["next_cursor"]
    if not cursor:
        break

check = maintenance.parity(start_time="2026-09-01T00:00:00Z")
assert check["passed"], check

Use these independent deployment flags:

Setting Default Meaning
MEMORIZZ_OBSERVABILITY_DUAL_WRITE false Store the canonical envelope and write its normalized native index
MEMORIZZ_OBSERVABILITY_READ_PATH bundles bundles compatibility path or explicitly selected index path

Enable dual-write only after provisioning. Start it before backfilling so newly arriving events are covered; repeat the final parity check on a quiescent window or with producers briefly paused. Cut over reads only after parity and the live-provider/reliability gates pass. The flags do not grant permission to migrate or delete data. Index readiness by itself is not proof of full backfill.

Parity compares exact, content-free event projections, not just row counts. It is bounded by max_pages (default 100 pages of 1,000 events); a larger or incomplete window fails closed. Narrow the time/tenant scope or increase the bound explicitly. Do not compare an expired index against unexpired source history and interpret deliberate retention differences as capture failures.

Rollback: set MEMORIZZ_OBSERVABILITY_READ_PATH=bundles. Backfill never rewrites the originals. Leave dual-write enabled while investigating or disable it explicitly. Once old source envelopes are expired, rollback for that historical window requires the archive; the live bundle path alone cannot reconstruct it.

Query contract

page = provider.query_trace_events(
    agent_ids=["agent-1"], memory_ids=["memory-1"],
    application_id="app-1", user_id="user-1", thread_id="thread-1",
    resource_refs=["analysis-1"], limit=250,
)
next_page = provider.query_trace_events(
    agent_ids=["agent-1"], memory_ids=["memory-1"],
    application_id="app-1", user_id="user-1", thread_id="thread-1",
    resource_refs=["analysis-1"], limit=250, cursor=page["next_cursor"],
) if page["next_cursor"] else None

Agent and memory are alternatives; application, user and thread scope are conjunctive. Omitting user_id is an unscoped SDK query; user_id=None selects anonymous evidence only. SDK callers must supply authorized scope. UI and MCP tools bind scope independently and never let model-provided IDs override it. Additional filters: root_trace_id, run_id, turn_id, event_kinds, statuses, tool_name, success, query (exact identifier), start_time, end_time. Native cursors bind timestamp plus event key and query scope; reject cursor reuse under different filters. New head insertions do not duplicate continuation rows.

index.summaries(...) aggregates event/bundle counts in the database without deserializing historical source payloads. The UI uses bounded aggregate groups in index mode (first 1,000 agent/thread groups), and separately loads a bounded event window. Overview and timeline counts consequently have distinct scopes. window_complete=False on a continuation page is deliberate; its absence of errors does not make it a complete incident.

Separate retention and source expiry

Default native policies are 7 days for previews, 30 for metadata, and 365 for verified outcomes. Policies require previews <= metadata <= outcomes.

print(maintenance.retention())  # dry-run counts only
# After reviewing the policy and target:
maintenance.retention(previews_days=7, metadata_days=30,
                      outcomes_days=365, dry_run=False)

New dual-written canonical envelopes are metadata-only. Raw previews have one separately expirable native copy. Content is capped at 16,000 characters and is never included in index queries or summaries. An idempotent retry cannot repopulate a purged preview while its bundle checkpoint exists. Historical backfill can reintroduce expired telemetry; use a recent since and reapply retention before enabling operator access after a rebuild.

Old/non-dual-written bundles may still contain previews. Native retention does not scrub those immutable sources. Review and archive/expire their exact envelopes separately:

plan = maintenance.plan_source_expiry(before="2026-08-01T00:00:00Z", limit=100)
# Review plan entries and ensure indexed metadata/outcomes are retained first.
# archive(payload) must durably preserve this envelope and return exactly True.
# The archive needs its own access controls and retention policy.
result = maintenance.apply_source_expiry(
    plan, confirmation=plan["confirmation"], archive=archive,
)

This is an explicitly destructive maintenance API, not a UI action or automatic background job. It rejects changed fingerprints, requires archival acknowledgement, and compare-and-deletes the exact source record (including Mongo physical IDs). Only trace bundles in the reviewed plan are eligible; conversation/semantic memory is not deleted. A multi-record failure may leave an archived, partly applied batch; inspect the provider and generate a fresh plan. Archives, backups and old replicas are outside the native preview purge boundary.

Explainable memory selection and worker coverage

Runtime deduplication records up to 64 candidates: typed opaque resource ref, provider rank/score, selected flag and the actual selection/rejection reason. Reasons distinguish MMR/provider ranking, conversation history, exact/near/parent duplicates and budget exhaustion. The runtime does not invent provenance or claim to have observed candidates rejected inside a remote provider.

Hosts can use SelectionDecision and recorder.record_selection(...) to record canonical-source, wrong-thread/tenant, stale-version or application policy decisions. The Memory Lineage view separates expected, retrieved, supplied and output-bound refs. Explicit reference metadata is required; prose/model claims are not proof.

from memorizz.observability import register_coverage_profile, ObservabilityRecorder

register_coverage_profile(
    "document_workflow", required={"intent_plan", "artifact_persisted"},
    optional={"ui_delivery"},
)
recorder.record_intent("document", expected_artifact_types=["doc"],
                       coverage_profile="document_workflow")

# After the host validates job ownership and obtains an authorized fallback scope:
worker = ObservabilityRecorder.for_worker(
    provider, job_trace_carrier, fallback_context=authorized_context,
)

Register profiles during application startup in each process. Replacing an existing profile requires explicit replace=True. Required/optional stages are disjoint and bounded; missing required stages mean partial coverage. A missing or invalid worker carrier emits trace_context_missing in the authorized fallback scope, or raises if no fallback was supplied. Carriers never authorize resource access. Intent, parser, browser acknowledgement, current ownership and artifact persistence remain host-reported facts; the SDK cannot observe these without host integration.

Scoped operator roles and host hooks

Existing MEMORIZZ_UI_AUTH_TOKEN remains an unscoped administrator token for backward compatibility. For multiple operators, configure MEMORIZZ_UI_AUTH_ACCOUNTS as a JSON object of operator names to token, role, optional application_id, and optional user_id:

{
  "reader-1": {
    "token": "replace-with-a-long-random-secret",
    "role": "viewer",
    "application_id": "app-1",
    "user_id": "user-1"
  }
}

Remove the legacy admin token when it is not needed. Tokens must be unique and at least 16 characters; use generated secrets in deployment. A present null user_id binds an anonymous account. Omitted scope is broad access within the other configured constraints—do not omit it accidentally. Cookie sessions are signed, expire and resolve the operator's current configured role. Configure the provider with the host/admin connection flow before handing scoped operators a trace URL; they cannot use connection/settings or generic memory pages.

Role Metadata trace reads Current artifact lookup / replay draft Transient account resolution Raw reveal
viewer yes no no no
analyst yes yes no no
operator yes yes yes no
admin yes yes yes yes, if content mode allows

Any tenant-bound account is limited to trace routes. MEMORIZZ_UI_READ_ONLY=true also blocks source/index/draft writes, including nested native-index APIs; read-only users can export an inert replay plan. Explicit query scopes may only narrow configured scope. Cross-origin trace POSTs are rejected. Metadata is the default on the page; content is fetched only by an explicit, audited reveal. The existing export endpoint remains an explicit administrator content action when its content mode allows it. Configure a writable private audit destination: privileged lookups, reveals, replay and raw export fail closed if auditing fails.

Embed host integrations through create_app(identity_resolver=..., artifact_resolver=..., resource_authorizer=...). Hooks can be sync or async:

Hook Input Required result
identity resolver transient email, authorized application/user scope bounded opaque user_id and optional application_id in scope
artifact resolver typed resource ref, authorized scope ownership_verified=True; optional exists, version, title_fingerprint
resource authorizer frozen typed ref/version, trace tenant scope exactly True after checking current access

An absent hook returns 501; lookup failures return content-free errors. Emails are sent only in POST bodies, cleared from the input, not stored as traces or in audit/query strings, and never echoed in validation errors. Hosts/reverse proxies must also disable sensitive request-body logging. The SDK does not connect to an account directory or application artifact database automatically.

Operator endpoints

Endpoint Purpose
GET /traces/find.json Bounded metadata-only Incident Finder, exact opaque q, IDs/time filters and cursor
GET /traces/inspect.json Captured lineage/artifact/contract inspection for agent + root
GET /traces/artifact.json Authorized current artifact state through the host hook
POST /traces/account/resolve Transient {"email": ...} resolution
POST /traces/reveal Explicit event preview; agent, root and event ID required
POST /traces/replays Authorized agent/root selection to an inert Evalground draft
GET /traces/replay-plan.json Same authorization/frozen refs without persistence
GET /traces/health[.json] Capture/query/index/registration health and deterministic alerts
GET /traces/compare[.json] Structural baseline/candidate differences, never execution

Replay freezes content-free refs and an evidence fingerprint. It requires a complete, trusted, single-tenant trace and current authorization for every ref, including rejected selection candidates. Unversioned refs are counted explicitly; they are not immutable content snapshots. Drafts have execution_enabled=false, network/side effects disabled, and sandbox/operator-approval requirements. They appear as reviewable observability_experiment records; creating one does not invoke a model, tool, queue or application action.

The optional MCP memorizz_query_traces tool is off by default. Enable MEMORIZZ_MCP_SERVER_ALLOW_TRACE_QUERIES=true with the existing read scope and agent exposure policy. Tenant identity comes from the authenticated principal (explicit anonymous scope for an anonymous local caller), not tool arguments. explain=true adds captured lineage and deterministic analysis. There is no raw reveal or replay execution API over this tool.

Health and release gates

Health reports schema/instrumentation versions, child counts, missing links, registration mismatches, truncation, query latency and index readiness. Process counters expose source/index write failures, dropped metadata and callback failures, with bounded latency samples. No counters from another process means unknown, not zero. Hosts can forward pipeline_health(provider), recorder.health, and deterministic health alerts to their monitoring service. Instrumentation deployment versions must be supplied by the host in allowlisted event attributes.

Run the normal regression suite and the offline workload:

PYTHONPATH=src python -m pytest tests/unit tests/integration -q
PYTHONPATH=src python examples/observability/host_workflow.py
PYTHONPATH=src python examples/observability/benchmark.py --events 10000 --max-p95-ms 250

The synthetic SQLite benchmark uses 500-child bundles, verifies 250-event bounded pages, aggregate/event parity, no cursor duplicates and no normalization errors. The unit gate also asserts that summaries never deserialize trace payloads and native reads never expand old source bundles. The audit-remediation rerun recorded p50 10.677 ms, p95 18.673 ms and 1.321 seconds total indexing for 10,000 events. These are local measurements, not production SLAs or MongoDB/Oracle performance claims.

Live databases (mandatory before native production cutover)

The integration gate is opt-in and never starts Docker or discovers endpoints. Supply isolated disposable targets using environment variables:

export MEMORIZZ_OBSERVABILITY_LIVE=1
export MEMORIZZ_OBS_TEST_MONGODB_URI="mongodb://127.0.0.1:27018"
export MEMORIZZ_OBS_TEST_ORACLE_USER="MEMORIZZ_OBS_TEST_RELEASE"
export MEMORIZZ_OBS_TEST_ORACLE_DSN="127.0.0.1:1522/FREEPDB1"
# Set MEMORIZZ_OBS_TEST_ORACLE_PASSWORD through your secret manager.
PYTHONPATH=src python -m pytest tests/integration/test_observability_live.py -q

Mongo creates and drops one randomly named test database. Oracle requires an empty dedicated MEMORIZZ_OBS_TEST_* schema and removes only the five index tables it creates. Never supply a production or host-application schema. With the live gate enabled, missing configuration fails rather than silently skips. Without opt-in, the eight cases (four per database) explicitly skip. To validate one supplied target at a time, add -k mongodb or -k oracle; the selected backend still fails on missing configuration.

For a local MongoDB gate without Docker or an existing server, obtain a trusted standalone mongod binary for your platform and verify its vendor checksum. The official macOS tarball instructions describe the available standalone package. No system installation is necessary:

PYTHONPATH=src python tests/integration/run_observability_mongodb.py \
  --mongod /absolute/path/to/mongod

This launcher does not download software, start Docker or load a system config. It starts one loopback-only child on an ephemeral port with temporary storage, checks both the server PID and database path before tests can write, and runs only the MongoDB cases. It overrides any inherited MongoDB test endpoint, stops only its own child (including on failure), and removes the synthetic test data. The binary is retained for reuse; no application database or service is used.

On 5 September 2026, all four cases passed against standalone MongoDB 8.0.26 on macOS arm64/Python 3.12.13: the 74-event two-tenant contract, concurrent idempotent retries and scope-bound cursors, durable interrupted-write recovery, and typed resource lookup with dry-run/apply retention. Nine offline launcher tests cover ownership checks, endpoint isolation, cleanup and failure paths. This is real-server standalone validation, not Atlas/replica-set failover or a production workload benchmark. The four Oracle cases subsequently passed on the newly authorized isolated local Oracle instance and disposable MEMORIZZ_OBS_TEST_LOCAL schema. See examples/observability/ORACLE-LOCAL.md for its connection and repeatable test commands. Docker was already running; only the new isolated container was created. OpenSpeech and all existing application containers remain untouched.

Actual deployed runtime credentials, source-store compatibility, concurrent producers, failover and retention/backup policy must additionally be checked in the target environment.

Browser gate

With a local Playwright installation and its Chromium browser available:

PYTHONPATH=src python tests/browser/observability_app.py
# In another terminal (Node >=18):
node tests/browser/observability.cjs
# Stop the fixture, then exercise unconfigured hooks and legacy evidence:
MEMORIZZ_BROWSER_TEST_HOOKS=off PYTHONPATH=src python tests/browser/observability_app.py
# In another terminal:
node tests/browser/observability_audit.cjs

The server binds localhost:8779, uses a temporary filesystem store and synthetic data, and does not load or connect to application services. The browser validates search/navigation, lineage, lane filtering, artifact lookup, explicit redacted reveal, inert replay creation and mobile layout. Override the module/browser path with MEMORIZZ_PLAYWRIGHT_MODULE / MEMORIZZ_BROWSER_EXECUTABLE if needed. Stop the fixture server after validation; its temporary data is cleaned up.

Both fixtures include a busy 40-thread overview. The configured-hook gate also uses a reused event ID in a second turn, checks the actual reveal/replay request scope and verifies that the draft excludes the other turn's resources. The audit browser gate checks unknown end-to-end coverage, explicit missing-evidence panels, disabled hooks, scoped navigation round trips, and incident placement within the first 1,200px on desktop / 1,400px on mobile. Width and absence of JavaScript errors alone are not treated as sufficient mobile usability evidence.

Before release, record the exact commit/wheel, provider versions, parity window, load distribution, browser result, migration identity and rollback decision. Repository implementation and synthetic validation do not certify production deployment, repair historical artifacts, or fill host instrumentation gaps.