Implemented architecture
MCP Flight Recorder
Package 0.5.16 · SQLite schema 6 · inspection format mfr.inspect/1 · updated 1 November 2026
MCP Flight Recorder captures explicit observations at model, host and MCP boundaries. The host owns native work; the recorder owns bounded capture and its recording. The HTML map renders the same implemented behavior offline. Detailed control flows contain nine source-linked views, with closure and snapshot drawn separately. This map covers the responsibilities and material branches of all 31 runtime modules, the schema and build seam; it is not a line-by-line call graph. Public details are in Capture, Inspection and Integrations.
Explicit evidence
Recorded identities and positive host observations establish relationships. Equal content does not establish provenance.
Accepted ≠ durable
Admission freezes, charges and queues work. Only a successful outer COMMIT makes surviving records durable.
Host-owned execution
The host owns clients, retries, caches, stream reads and cleanup. Optional facades observe their supported public calls.
Portable ≠ complete
A verified, portable database can still contain partial capture. Readers preserve missing evidence and uncertainty.
M00
Component boundaries and ownership
Scroll sideways when needed. Keyboard: focus the diagram, then use Left and Right.
Invariants and source entry points
Entrypoints: Recorder; Writer.run; run_read; snapshot; openai_chat.wrap_model, anyllm_ollama.wrap_model, mcp_client.wrap_mcp, mcp_session.wrap_session, mcp_session_legacy.wrap_session.
Invariants: The host owns native SDK work and client lifecycle. Optional facades observe native calls; direct Recorder calls bypass the facades. The capture-time writer owns its write connection. Inspection and snapshot use separate read-only source connections. Snapshot writes only a new destination; admission is not durability.
Supported observation paths
| Public path / observation | Recorded fact and boundary |
|---|---|
turn(...) / dispatch(...) |
Host scope; dispatch action is execute, refuse, local or defer, with explicit source decision-event references. |
observe("model.request", ...) |
New model-call identity; structured manifest or explicitly selected direct request body. |
observe("model.tool_call", ...) |
Separate decision occurrence and event identities; original argument comparison and visible unresolved references. |
observe("model.response", ...) |
Complete, partial, error or cancelled source response. A response is never invented from a missing observation. |
start_operation(...) / finish_operation(...) |
mcp.request/mcp.response plus one operation lifecycle, explicit attribution, and a reserved completion ticket. |
observe("host.tool_result", ...) |
Host-prepared material, explicit source-slot/pointer witnesses and optional ordered text comparison. |
observe("cache.served", ...) |
Positive host/client cache observation, optional cache/client/operation identifiers and finite nonnegative TTL. A repeated body or absent network event is not a cache witness. |
Decision occurrence IDs and decision event IDs are different. Explicit insertion validation checks that the original destination exists and equals the submitted material. A symbolic source can remain unresolved; its link does not prove source existence. The adapters observe only their supported public SDK seams, described in Integration boundaries.
M08
Initialization and turn scope
Scroll sideways when needed. Keyboard: focus the diagram, then use Left and Right.
Invariants and source entry points
Entrypoints: Recorder.__init__, _ScopeContext, Recorder.capture_scope, Recorder._current_scope_frame, Recorder._validate_scope; constructor_config, coverage_config, limits_config; Writer._reserve_path, Writer._initialize.
Invariants: New recording only; declared coverage is not observed completeness. Initialization commits before readiness. Private ambient frames hold immutable public scopes; rejected turns stay disabled and rejected dispatches clear only the dispatch association. Entry acquires its restoration predecessor after receipt preparation. Normal exit resets locally without retiring inherited frames. Foreign reset retires only the exiting frame; immediate normalization requires that exact current frame, while other callers normalize only their retired prefix on lookup. Explicit snapshots and tickets stay valid. Exit observation precedes restoration, with host outcomes and receipts preserved. Configuration defaults and all observation paths appear in §§2–3.
Initialization and scope ownership
Recorder creates a new recording. It validates explicit capture="content" or "metadata", boolean comparisons, declared model/host/MCP producer IDs, detached environment JSON and supported positive limits. A boundary with a null producer cannot emit observations; model_complete=True requires a model producer. These declarations describe intended coverage, not proof that every real-world action was observed.
The parent directory must exist. The writer refuses an existing main, WAL, SHM or journal path, reserves a new mode-0600 main exclusively, installs schema 6 and commits the initial run before constructor readiness. Initialization failure raises; observation failures after initialization use capture receipts and health. There is no append/reopen or migration runner.
The recorder's ContextVar holds a private _AmbientScope frame or None; public CaptureScope values remain sealed and immutable. A frame's scope and predecessor are fixed at installation. capture_scope uses _current_scope_frame to skip only a foreign_closed prefix, stopping at the first live frame or None and updating only the caller's context when needed. A live descendant, disabled turn or rejected dispatch stops traversal. At None, the lookup issues the normal sealed unscoped value without installing a frame.
Entry first captures the public predecessor for receipt attribution. An accepted turn establishes its new turn with no dispatch; a rejected turn establishes a disabled scope with null IDs. Dispatch preserves the earlier turn and disabled state, carrying an admitted dispatch event or clearing the dispatch ID on rejection. After receipt and public-scope preparation, entry acquires the normalized predecessor frame immediately before installing its new frame and token; no observation or callback runs between those steps.
Exit observes the existing outcome before restoration. An accepted turn attempts returned, cancelled for CancelledError, or error for other exceptions including GeneratorExit; a rejected turn returns rejected/invalid_scope without an end update. Dispatch exit, including rejected entry, returns an accepted receipt with its scope's turn ID and writes no terminal event. Successful token reset restores only the local context and never retires the frame, preserving inherited child scopes. Only ValueError from the reset call marks the exiting frame foreign_closed; the closer normalizes immediately only if its current frame is exactly that frame. Any different closer frame stays untouched, and the origin normalizes its retired prefix on its next lookup. Other reset errors and exit-observation errors are not caught by this recovery. Scope exit returns False and preserves host exceptions. Previously issued explicit snapshots and operation tickets remain valid after ambient retirement. Turn conditions are detached and receive a deterministic sorted-JSON SHA256 condition key. See M08.
M01
Preparation, admission and omissions
Scroll sideways when needed. Keyboard: focus the diagram, then use Left and Right.
Invariants and source entry points
Entrypoints: Recorder._capture, Recorder._admit, Recorder._publish, Recorder._reject; Prepared.bundle; Artifacts.omit_prepared.
Invariants: Each call owns its Preparation and lazy identity-read scope; autocommit statements refresh visibility, and cleanup precedes admission and logging. Cleanup failure rejects without publication. Preparation occurs outside admission and provides exactly three variants, indexed 0, 1 and 2. Each admission attempt rechecks accepting state under the condition lock before capacity and ticket checks. Starts reserve terminal capacity separately; terminals need a valid active ticket and sufficient metadata budget. Omission/remapping may leave incomplete evidence. Queued and writer-held work stays charged; admission publishes pending, not durable, identities.
Capacity defaults and three admission variants
| Limit | Default | Ownership |
|---|---|---|
| Ordinary work | 256 slots / 8 MiB | Queued and writer-held ordinary charges. |
| Completions | 64 slots / 256 KiB | Accepted starts reserve one slot and 4096 bytes each; the per-start 4096-byte reservation is fixed. |
| Writer batch | 64 bundles / 1 MiB | The first bundle remains indivisible. |
| Coalescing / SQLite busy timeout | 20 ms / 1000 ms | Empty-queue waiting / SQLite lock waiting. |
| Committed identity cache | 512 entries | Configurable committed_cache_items; bounded loss-turn tracking uses the same limit before escalating to run-wide loss. |
The listed limits are configurable positive integers, except the fixed terminal reservation. They are not bounds on total capture time, callbacks, caller objects, SQLite buffers or process RSS.
Admission tries at most three prepared variants: 0, the original bundle; 1, body omission with repaired references and retained ordered membership; 2, structural omission. For a structured request, variant 2 omits the manifest body and answer candidates, retains directly referenced placeholders and explicit omission counts, and marks manifest_omitted. It never silently shortens membership. A terminal can discard its optional artifact and retain the reserved minimal event/update with an explicit capacity omission. If the minimal form cannot fit, capture rejects and counts loss. These are bounded capacity alternatives, not background retries.
A terminal uses the existing valid active ticket and reservation. Its optional body still consumes ordinary bytes, but it needs no additional ordinary slot. Duplicate/foreign tickets and an oversized terminal control envelope reject. Admission and release remain atomic under their existing ownership rules. See M01.
M02
FIFO writer, savepoints and transaction outcomes
Scroll sideways when needed. Keyboard: focus the diagram, then use Left and Right.
Invariants and source entry points
Entrypoints: Writer.run, Writer._next_batch, Writer._savepoint, Writer._transaction, Writer._record_closure; Recorder._commit_bundles, Recorder._discard_bundle, Recorder._prerequisite_failure, Recorder._writer_failure.
Invariants: Ready FIFO candidates drain to the existing item/byte caps; the first bundle is indivisible. The coalescing deadline bounds only empty-queue waiting, and stop drains ready work without waiting. Selection retains ordinary charges and terminal reservations through COMMIT/discard. Per-bundle savepoints sit inside one outer transaction. An identity-free logical rejection can be isolated and its charge released without a successful COMMIT. An identity-bearing or turn.start rejection discards the dependent suffix and stops admission; an earlier valid prefix may still commit. Only successful outer COMMIT makes surviving identities durable. Unexpected transaction/commit failure rolls back and fails capture. Each operation start/row and terminal/row update is atomic.
M03
Request manifests, identity and explicit links
Scroll sideways when needed. Keyboard: focus the diagram, then use Left and Right.
Invariants and source entry points
Entrypoints: Preparation._request, Artifacts.request, Artifacts._message_parts, Artifacts._message, _fields, original_digest, Artifacts._template_parts, _template_copy, Artifacts._remap_manifest, Artifacts.omit_prepared, request_info, answer_rows.
Invariants: request_refs/2 preserves message order and duplicate occurrences. Each message validates annotations/bindings before field preparation; comparison uses the original field size, and the final artifact row reuses those fields. The reader member relation filters valid bodies, retains the guarded iterator/format check and extracts projected paths directly without changing their SQLite scalar types. Complete compatible original, binding and retention facts support identity reuse; incomplete or unknown facts do not. Capacity omission can remap artifacts and bindings. The first committed witness establishes a binding. dispatch_source, answers, transformed_from and inserted_into require explicit observed facts; a symbolic source reference does not prove source existence. Provider IDs, matching bodies/names and timestamps create no provenance.
M04
Model observations and host-consumed streams
Scroll sideways when needed. Keyboard: focus the diagram, then use Left and Right.
Invariants and source entry points
Entrypoints: policy_retention, _retained_request, ModelCall.observe, openai_chat.wrap_model, anyllm_ollama.wrap_model, ModelCall.request, ModelCall.decision, ModelCall.terminal, StreamCapture.consume, StreamCapture._emit_tool, read_chunk, cleanup_stream.
Invariants: Wrapper invocation returns a deferred coroutine; native work and observation begin when awaited. The host drives every stream read and cleanup, without prefetch or forced draining. Normal return, error/cancellation, EOF-complete, early close and partial/gap outcomes remain distinct. Raw-body loss makes the aggregate partial; it does not itself make tool selection incomplete. Raw admission and omission rejoin selection inspection under its independent fragment pool and 64-choice/64-lifetime-key limits. Actual projection/selection gaps remain conservative; fragment or cardinality overflow stops further capture while native chunks still reach the host. Complete tool decisions require a supported function shape, usable name/argument fields, complete observed fragments and an accepted tool finish reason. Argument-JSON comparison availability is a separate check; gaps and early cleanup cannot manufacture completeness. Each available request, decision and response has a separate detached policy selection before observation; structured requests share core validation. Ordinary policy failure selects omission once without making complete source evidence partial. Process-control interruption can prevent native work, replace an outcome or withhold a consumed chunk; catching and resuming does not gain a cleanup or at-most-once policy guarantee.
Supported SDK seams
| Facade | Supported seam and projection | Boundary to retain |
|---|---|---|
| OpenAI Chat | A bound async Chat Completions method; supplied ordinary JSON and trusted native model/chunk projections. | Does not synthesize SDK defaults or transport options. Unsupported legacy/overriding inputs can make request capture unavailable while native inputs remain unchanged. AsyncStream has iteration, context, response and close forwarding. |
| AnyLLM/Ollama | Bound acompletion; supplied bound args and allowed provider options; normalized OpenAI response/chunk types. |
Excludes timeout/session label from projection; unknown provider options can make it unavailable. Wraps only a closeable async iterator, exposing iteration/aclose. Complete stream tool decisions accept stop or tool_calls. |
| MCP 2 Client | Public list_tools / call_tool; list options include cursor, meta and cache mode. |
Observes public invocation/result; client, cache, retries and lifecycle remain host-owned. |
| MCP 2 ClientSession | Public list/call methods with trusted typed pagination projection. | A caller-allowed InputRequiredResult remains incomplete rather than a final text-result claim. |
| MCP 1 ClientSession | Explicit separate legacy module and native types/signatures. | Preserves legacy cursor/params, isError, McpError.error, timeout and httpx translation. No automatic version detector. |
M05
MCP observation without invented attribution
Scroll sideways when needed. Keyboard: focus the diagram, then use Left and Right.
Invariants and source entry points
Entrypoints: policy_retention, _OperationObserver._retention, _OperationObserver._start, _OperationObserver._finish, _OperationObserver._execute, mcp_client.wrap_mcp, mcp_session.wrap_session, mcp_session_legacy.wrap_session, Recorder.start_operation, Recorder.finish_operation.
Invariants: Native MCP work executes once despite ordinary capture or policy faults. Detached policy output or fixed failure omission precedes each applicable single capture attempt; a terminal is attempted only for an admitted start with a valid ticket. Process-control interruptions propagate and may prevent native work or its terminal observation. Attribution is host_hook, propagated or none, based only on explicit observed evidence. Method/name context is invocation-local. The host owns transport, retries and cleanup; modern and legacy SDK translations remain distinct.
M06
Bounded, read-only inspection and uncertainty
Scroll sideways when needed. Keyboard: focus the diagram, then use Left and Right.
Invariants and source entry points
Entrypoints: run_read, finish_envelope, _slice, _check, query, cli._dispatch.
Invariants: Invalid arguments, including timeout, can return a JSON error before opening a connection. One bounded snapshot cache preserves occurrence completeness and diagnostics; eviction of a membership minimum permanently restores the original authority path. Cache bounds are not RSS bounds. Each reader invocation uses its own bounded read-only transaction. Checks distinguish pass/fail/unknown, slices use BFS with frontier/cutoff, and SQL is read-only under validation and an authorizer. Only the SQL route sets the 8,388,608-byte SQLite string/BLOB/encoded-row cap after trusted setup and leading-keyword validation, before caller authorization/preparation. A native too-big error returns limit_exceeded/sql_value_limit with no partial data; an expired deadline takes precedence. Successful native NULL stays in rows. Authorization remains active through statement cleanup, followed by rollback/close. This cap is independent of output/cache budgets, does not bound all query allocations or temporary sorting, and does not isolate hostile SQL or bound process RSS. Large stored values may also refuse through length/substr; other readers and payload prefix access are unchanged. Relationships and omissions remain available, invalid or unknown. Rollback/close applies only if opened. CLI snapshot is independent of run_read.
Reader and CLI behavior
Eight Python reader entrypoints share the inspection contract. fr snapshot is a separate copy operation; python -m mcp_flightrecorder uses the same CLI dispatcher. See M06.
| Python / CLI | Behavior and important branch |
|---|---|
schema() / fr schema |
Static installed contract. Opens no recording. |
runs() / fr runs |
Ascending (started_at, run_id) pages with one-row lookahead and continuation. Fixed 10-second read budget; no public timeout argument. |
find_turns() / fr turns |
AND-combined filters, exact retained counts and four positive-witness flags. Cursor binds path, run, filters, output policy and order. Every page is a new snapshot. |
explain() / fr explain |
Turn totals and model-call summaries; optional selected-call detail. Input answers and answers produced by selected decisions are separate. Produced occurrences can belong to later turns; unattributed operations stay visible. |
check() / fr check |
Nine integrity/availability items: model/host/MCP producer coverage, capture health, artifact storage, request structure, operation lifecycle, links and answer bindings. Overall fail outranks unknown; empty applicable populations may be not applicable. ok=true means the read completed, not that checks passed. Tool errors and valid argument/text differences are facts, not automatic integrity failures. |
slice() / fr slice |
Authoritative request root, bounded breadth-first ancestry through explicit relations/operation identities, deduplication and context-only witnesses. Root is preserved; missing evidence and record/depth/pending/output limits remain in frontier/cutoffs. complete concerns ancestry/traversal; unknown text comparison can coexist with complete structural ancestry. |
payload() / fr payload |
Explicit retained body only, using intact UTF-8 byte windows and next_offset. Unknown/omitted artifacts, invalid offsets and offsets inside a character refuse. no_content rejects before access. |
query() / fr sql |
One explicit SELECT/WITH; supported scalar parameters/results; SQLite action/function allowlist; row/output/deadline limits and a query-only 8,388,608-byte SQLite value/encoded-row cap after trusted setup. Native too-big errors return limit_exceeded/sql_value_limit; successful native NULL stays a row value. No writes, ATTACH or PRAGMA. Unsupported output values refuse; no_content rejects before opening. |
fr snapshot |
Independent read-only source → private backup/finalization/verification → exclusive publication → private cleanup. It preserves source state and capture incompleteness. See M07. |
CLI exit status is 0 for successful ordinary commands and passing/not-applicable checks, 2 for a failed check, 4 for an unknown check, 3 for incompatible/unreadable/open-WAL recording errors, 1 for other errors, and 130 for KeyboardInterrupt.
M07
Recorder closure versus independent snapshot
Capture closure. The caller and writer have separate deadlines/ownership.
Scroll sideways when needed. Keyboard: focus the diagram, then use Left and Right.
Independent snapshot. A published main is never removed to disguise an uncertain or failed outcome. A private cleanup failure can replace prepared success with output_cleanup_failed.
Scroll sideways when needed. Keyboard: focus the diagram, then use Left and Right.
Invariants and source entry points
Entrypoints: Recorder.close, Recorder.aclose, Recorder.finalize, Writer._record_closure, Writer._finish_storage, retry_finalization, snapshot, _check_destination_available, _publish_staged.
Invariants: Close waits within a finite budget and can time out while the writer continues. Only joined, verified closure reports portability, not complete capture. The writer records closure and attempts checkpoint/DELETE; retry finalization uses a separate owner after the writer stops. Snapshot probes the source read-only before output checks, then backs up, finalizes, verifies and prepares its response in private same-filesystem staging. An exclusive hard link publishes the complete main file without replacement. Link uncertainty or a visible post-link sidecar can return an error with a public main already present. Cleanup touches only private staging, never public main/sidecars; the post-link check is best effort and source bytes remain unchanged.
How capture completeness is decided
Closure drains accepted work. Running operations become incomplete without a fabricated response or end time. Missing turn ends, unmatched model requests, incomplete operations, rejected or discarded capture, and capacity, unresolved or partial-source losses make affected turns partial. Ordinary policy or metadata body omission alone does not. Unscoped loss or unmatched work conservatively affects the run's turns; bounded turn-loss tracking can escalate to run-wide loss. Partial/error/cancelled model responses already mark their source capture partial. The closure transaction saves health and the run's clean or incomplete state before storage finalization.
Storage portability is a separate decision. finalize() retries storage finalization only after the writer has stopped; it does not repair missing observations, replay host work or turn an incomplete recording into complete evidence. See M07.
1. Responsibilities and data flow
Host observations enter frozen capture preparation through either a thin optional SDK facade or the direct Recorder.observe / Recorder.start_operation API. Both paths continue through atomic admission, the capture-time FIFO writer and SQLite. Reader invocations inspect SQLite through separate read-only connections. Snapshot has its own read-only source connection and writes only its private staged copy and new destination.
| Layer | Current source | Responsibility |
|---|---|---|
| Public capture | __init__.py, _recorder.py, _types.py, _config.py |
Public imports, scope, receipts, admission, bounded health and shutdown |
| Capture preparation | _capture.py, _artifacts.py, _values.py |
Freeze ordinary JSON, prepare envelopes, projections, retention and request manifests |
| Writer and finalization | _storage.py |
Capture-time write-connection ownership, FIFO transactions, identities and reservation release; a post-stop finalization retry owns a separate connection |
| Model adapters | openai_chat.py, anyllm_ollama.py, _model_capture.py, _model_stream.py |
SDK translation, shared model/stream lifecycle and optional model body policy |
| MCP adapters | mcp_client.py, mcp_session.py, mcp_session_legacy.py, _mcp_capture.py, _mcp_native.py |
Version-specific native translation around one shared operation lifecycle and optional MCP body policy |
| Evidence guards | integrations/_common.py, integrations/__init__.py |
SDK-free namespace; label/policy validation, detached JSON copying, single-use evidence claiming and bounded capture failures |
| Reader | inspect.py, _inspect_common.py, _inspect_records.py, _inspect_relations.py, _inspect_checks.py, _inspect_navigation.py, _inspect_query.py, _inspect_slice.py |
One bounded read transaction and deadline per invocation; shared projections and complete retained-fact counts; static schema metadata needs no connection |
| CLI and copy | cli.py, __main__.py, _snapshot.py |
JSON commands over the reader, a separate run-selection read when needed, and an independent portable snapshot utility |
| Operational logging | _logging.py, component module loggers |
Best-effort standard logging with fixed diagnostic fields; handler failures cannot replace capture or inspection outcomes |
| Storage definition | sql/schema.sql, setup.py |
One canonical schema copied into the installed package during the build |
The five public SDK facades are OpenAI Chat, AnyLLM/Ollama, MCP Client, MCP ClientSession and legacy MCP ClientSession. Neither the core nor the reader imports an SDK. Source examples show host-specific attachment; they are not installed runtime modules.
Detailed contracts and implementation notes
Open the contract you need. These details retain the current semantics, ownership rules and performance limits; the visible views above summarize the same implementation.
2. Observation and identity
The recording distinguishes submitted input, selected tool arguments, host dispatch arguments, actual MCP invocation, observed outcome, and material submitted back to model context. A dispatch can refuse, defer or handle work locally. An MCP operation can lack model attribution. These facts remain separate rather than being collapsed into a presumed tool-call chain.
Core-issued receipts carry event, model-call, decision and operation identities. Decision occurrence IDs and decision event IDs have different uses. Only explicit recorder references or a positively observed host hook establish attribution. Matching names, payloads, timestamps or provider IDs never establish it.
Scope is captured when a wrapper is invoked and retained through deferred execution. Payloads are frozen when execution exposes them. Constructing a coroutine that is never awaited creates no observation or completion reservation. Awaiting a model call can record its request before the host consumes any returned stream. Host work and stream cleanup remain the host's responsibility.
3. Bounded admission
Each capture owns a fresh Preparation and _IdentityReadScope. Identity lookups first check pending/committed memory, then lazily reuse at most one calling-thread read-only connection. Autocommit statements see later commits; no miss or pending value is promoted into committed memory. The committed cache is bounded by limits["committed_cache_items"], with 512 entries by default. Cursors and the scope close before admission/logging, including failures and nested captures.
Capture preparation produces immutable bundles outside the short admission critical section. The admission lock atomically checks capacity, queues a bundle and publishes its pending identities. Rejection publishes no identity. Accepted means frozen, charged and FIFO-admitted; it does not mean committed or durable.
Ordinary bundles and operation completions have separate byte and slot pools. An accepted operation start reserves minimal terminal capacity, so its terminal envelope can still be admitted when ordinary capacity is full. Queued and writer-held work remains charged. A reservation is released once, on commit, explicit discard or close-time handling.
The queue is bounded; health and loss diagnostics are bounded; optional body retention may be omitted explicitly. There are no per-event futures or unbounded background retries. Ordinary capture and model/MCP policy failures preserve native outcomes; policy process-control interruptions propagate as described in §6.
4. Transactions and shutdown
During capture, one writer thread owns the write connection. It writes a FIFO batch in an explicit outer transaction with one savepoint per bundle. Releasing a savepoint does not publish committed identity or release capacity. The successful outer commit establishes durable order and first binding witnesses.
The writer takes the first FIFO bundle intact, then drains already-ready work up to the existing item and byte caps. The first bundle remains indivisible even when it exceeds the batch byte cap. The oldest bundle's coalescing deadline limits only waiting for an empty queue: expiry does not stop selection of ready candidates. Once the queue is empty, the writer waits only while accepting and only for the remaining deadline; stopping drains ready work without waiting. Selection does not release ordinary charges or completion reservations; ownership continues through outer COMMIT or explicit discard.
An expected logical rejection can be isolated only when it loses no prerequisite identity. Rejection of an identity-bearing bundle or a turn start stops admission and discards its accepted dependent suffix; a valid earlier prefix can still commit while storage remains healthy. Unexpected storage, rollback or commit failure stops capture. No suffix is silently reattached to another scope.
An operation start event and its running row commit atomically. Its terminal event and exactly one running-row update also commit atomically. Closure marks unfinished operations incomplete without inventing an observed outcome or end time. Unfinished model requests use a run-scoped, NULL-safe response-ID set query, preserving duplicate requests and NULL turn IDs. An unmatched unscoped request still makes all affected turns partial; closure transactions, receipts and the five-second default budget are unchanged.
The writer uses WAL and synchronous=FULL during capture. Portable closure requires a joined writer, closed owned connections, a successful checkpoint with busy=0, nonnegative log count and all log frames checkpointed, verified DELETE journal mode, and a fresh read-only open. Already-DELETE is valid. Busy finalization retains the files and reports its reason; no code manually deletes SQLite sidecars to force portability.
close() stops admission and waits within its finite budget. aclose() starts the same shutdown when its coroutine executes; executor queue delay consumes that caller deadline. Cancelling the asynchronous waiter does not retract shutdown or cancel host work. After the capture writer stops, Recorder.finalize can start retry_finalization on a fresh thread that opens and closes its own connection; it never borrows the writer connection. Portability and complete capture are separate facts.
5. Storage and request history
Schema 6 has seven tables: runs, turns, artifacts, events, operations, links and findings. The three views are observed_events, request_members and answer_occurrences. The current recorder and reader do not implement a scoring system or write findings.
| Stored table / derived view | Role and authority |
|---|---|
runs / turns |
Run policy, declared coverage and health; turn conditions, host outcome and capture state. |
artifacts |
Retained bodies, SHA256/byte count and explicit redaction/truncation/omission; original observed size remains separate. |
events |
Run-ordered observations and payload/catalog references; event, model-call, decision and operation IDs retain distinct meanings. |
operations |
One logical invocation, with atomic start/running row and terminal/update; unresolved closure preserves missing outcome. |
links |
Explicit relations committed with a witness event. Source/target IDs may be symbolic; they are not assumed foreign-key-verified evidence. |
findings |
Reserved analysis table in the schema. The current recorder/reader neither scores evidence nor writes findings. |
observed_events |
Authoritative observed version-1 events whose producer matches declared run coverage. |
request_members / answer_occurrences |
Ordered request membership, duplicate occurrences and first-witness answer associations, including the legacy explicit-link arm. |
| Relation | Explicit meaning |
|---|---|
dispatch_source |
A host dispatch identifies its source model decision event |
answers |
A submitted answer block identifies its decision and optional observed result |
transformed_from |
Host-prepared material identifies its source event/slot/pointer |
inserted_into |
A host observation identifies prepared material inserted at a verified request destination |
Only an explicit message_path=None selects a direct request artifact, with message_array_unavailable. Otherwise the default is /messages, and missing/malformed pointers, a non-array message value or overlapping extraction paths reject as invalid_input; they do not fall back to a direct body.
Requests use a flat request_refs/2 manifest: template, message path, ordered message handles and extracted field handles. Duplicate message occurrences keep their positions. The manifest does not repeat a cumulative answer list. Answer bindings use explicit links and a first committed witness; later requests expose their current occurrences through membership.
For a structured request, the first public freeze detaches payload and facts, including a distinct retained request. After extraction paths and retained structure are validated, _template_copy shallow-copies the root and copies the ancestor containers along each path in the evolving template, replacing only the message array and extracted fields with null. Earlier masks survive when paths share ancestors; dict keys, list indices, escaped pointer tokens and unrelated values keep their meaning and order. Unchanged descendants are borrowed from the detached snapshots only during synchronous preparation. Existing serialization completes the template, message and field artifact bodies before admission; no borrowed template container reaches the writer. The original snapshots remain unchanged for later insertion validation.
Template observed_bytes describes the masked original template; retained body bytes, lengths and hashes describe the masked retained template. Fields retain their specified order and catalog reference, and message occurrences retain every position. Complete equal originals with equal bindings and compatible retention can share a message handle. Distinct originals remain distinct handles even when their retained message bodies are equal. The first snapshot, defensive digest freeze, policy metadata, admission and writer ownership are unchanged.
Reusable message identity requires compatible complete original, binding, projection and retention facts. Incomplete or unresolved observations receive fresh handles. Known bindings survive partial uncertainty elsewhere in a message; unknown context is never converted to an empty known binding list. bindings: [] describes only a message actually known to be unbound. Unavailable annotations can force fresh handles and repeated retained bodies as cumulative history grows; storage cost cannot be reduced by inventing bindings or hiding loss.
6. Original comparisons and retention
Original values versus retained bodies
The public JSON boundary accepts exact built-in dictionaries, lists, strings, integers, finite floats, booleans and null. It detaches containers and rejects cycles, custom objects, non-string keys and nonfinite values. MISSING is distinct from JSON null and is allowed only at an exposed top-level absence boundary.
Original equality uses a random in-memory per-run HMAC key and the named args_json_python/1, text_blocks_json/1 and message_json/1 projections. Canonical typing distinguishes booleans, integers and floats; the encoded projection cap is 1 MiB. Disabled, incomplete, unsupported, oversized or unavailable originals yield unavailable tokens with reasons. Raw JSON arguments reject duplicate keys/nonfinite values; disagreement with supplied parsed arguments is conflicting_arguments. Ordered text paths define text comparison. Equality establishes neither provenance nor authentication.
Original observed size and comparison input remain original. Body text, stored SHA256/length and retention flags describe the retained value. Metadata capture takes precedence over policy-provided content. Retained JSON null differs from omission. Normal policy omission alone need not make an otherwise complete source observation partial. Capacity loss, unresolved binding and partial source observations remain explicit.
Named projections are args_json_python/1, text_blocks_json/1 and message_json/1. Typed canonical JSON distinguishes booleans, integers and floats. A per-run random 32-byte HMAC key exists only in memory; original comparison tokens are scoped to that run. The encoded comparison projection cap is 1 MiB. Tokens indicate equality of the specified complete observed originals, not provenance or authentication.
For each request message, annotation and binding validation runs first. _fields then prepares the existing artifact fields once, including the original compact UTF-8 observed_bytes and the retained body, hash and flags. _message passes that original byte count through the private digest calls and reuses the prepared fields in _artifact_row. Availability checks and the defensive comparison freeze remain. A replacement or omission changes retained fields, never the original comparison input or eligibility size.
An original message with known UTF-8 size n uses one-shot compact, sorted ASCII JSON encoding only when 6 * n <= 1_048_576; the actual encoded length must also stay within that cap. The sixfold bound includes DEL, whose one UTF-8 byte becomes six ASCII escape bytes. Larger or absent size hints retain streaming encoding and its early limit/error order. Argument and text comparisons remain unhinted. This reorders existing field preparation and reuses its measured size; it does not remove original or retained field serialization.
Public boundaries detach and validate ordinary JSON before preparation. The internal canonical encoder and original-digest helper retain recursive validation, including for JSON parsed from raw argument strings. This preserves unavailable comparison tokens and their reasons when deeply nested input exceeds that validation boundary. Diagnostic copies remain where subsequent operations mutate them.
Valid inputs that pass the first public freeze may now succeed where the redundant whole-request template freeze previously exhausted the recursion stack. Removing that incidental later failure does not establish a fixed nesting-depth guarantee. Unsupported types, cycles, nonfinite values, invalid, overlapping or missing extraction paths and retained-message reordering still follow the existing validation.
MCP and model selective retention provide opt-in synchronous callbacks for model requests, separate decisions, normal responses and consumed-stream aggregates. The initial snapshot, message processing, digest validation, policy execution and serialization run synchronously. Bounded message encoding reduces iterator and per-chunk HMAC work; it does not establish total capture-time or memory bounds. Throughput and process-memory capacity depend on the host workload, SDK behavior and retention callbacks and require separate measurement.
Content capture retains bodies by default. Metadata capture omits bodies while retaining envelopes, structural membership and omission facts. Original comparisons use detached original values independently of retained bodies; preparation order does not change that authority. Redacted, truncated, omitted and unavailable facts remain distinct; payload bytes are never copied into receipt or diagnostic fields to bypass retention.
The three MCP factories accept optional synchronous retention_policy(kind, payload, context). Request payloads contain the complete {"arguments": ...} envelope; terminal payloads contain the existing translated result or error body. Callback payload and context are detached copies, with exactly client_id, server_key, method and name in context. Returned mappings are detached and validated before capture. Native values and original comparison inputs remain unchanged; no policy-owned body reference reaches admission.
The default retention_policy=None adds no policy call or copy. Scope remains fixed at invocation; projection and policy run only when lazy work executes. Never-awaited work creates no policy/native call or reservation, and MISSING skips the callback without becoming JSON null. Ordinary policy faults and callback-raised cancellation select fixed adapter_policy_error / retention_policy_failed omission before one capture attempt. The bounded loss is logged once per invocation, without payloads or exception messages; no raw fallback or second native call is added.
For MCP policies, SystemExit, KeyboardInterrupt and other unhandled BaseException values propagate. A request-side interruption can prevent native work; a response-side interruption can replace its pending outcome and leave an unfinished operation and incomplete close. Ordinary policy faults preserve native return, exception and cancellation identity. Each call keeps its own method/name context; the host owns any shared callback state.
Core start_operation(retention=...) applies the existing mapping to the complete request envelope. Original argument/result-text tokens and observed_bytes describe originals; stored bytes, hashes and flags describe retained bodies. Explicit retained null differs from omission. Invalid direct-core retention rejects without a request or reservation. Global metadata capture always omits bodies with capture_metadata precedence, including when a policy returns retained content.
The two model factories accept retention_policy=None. Their shared ModelCall owner selects retention separately for model.request, model.tool_call and model.response, including consumed-stream aggregates. Context has exactly instance_id, choice_index, tool_index, name and provider_ref; all but instance_id are null for requests/responses. Decisions expose their original positions and bounded name/reference. Callback payload/context and returned mappings are detached; original event metadata and native values remain unchanged. Host attachment helper signatures are unchanged.
Model requests reuse the pure _retained_request rule from core before the one observation attempt. Retained /messages and any submitted /tools path must remain present; the message array keeps its count and cannot move a recognizable unchanged original message to another position. Per-position replacement is allowed, and retained tools have no added count/order rule. This is structural validation, not arbitrary semantic reorder detection. Ordinary mapping, encoding or topology faults select fixed omission before admission; direct-core invalid-retention rejection remains unchanged.
Original request membership, bindings, insertions and per-part observed_bytes remain original. The structural request_refs/2 manifest remains retained when content is omitted. Supported decision args_digest values compare equal originals within one run; retention-distinct message artifact IDs may differ. Stored lengths, SHA256 and flags describe retained bodies. No model-response text digest, catalog token or public message digest is added. Retained JSON null is valid for decision/response bodies but invalid for a structured request. Metadata mode still executes configured policies and preserves capture_metadata precedence, including after failure.
Model policy failures add retention_policy_failed once per invocation without changing source completeness solely for omission. Default None adds no policy call/copy; invocation fixes scope, execution projects bodies, and MISSING skips policy. Streams apply policy to emitted decisions and the final consumed aggregate, never each chunk. Original chunk identity/order and existing bounded accumulation, partial/error/cancelled state and cleanup remain; there is no prefetch or drain.
Model policy process-control exceptions propagate at request, decision and terminal boundaries. Request interruption can prevent native work; decision/terminal interruption can replace a pending result, withhold a just-consumed chunk, or leave a missing terminal and incomplete close. Catching an interruption and resuming the same stream has no new guarantee of automatic cleanup or at-most-once policy invocation. Ordinary faults remain contained. Model observations retain their model/event lifecycle, not MCP operation reservations.
7. Read-only inspection
Bounded output and uncertainty
mfr.inspect/1 returns ok, scope, data, unknowns, cutoffs, omitted-diagnostic counts, error and untrusted_content=true. Default output is at most 65,536 bytes, configurable from 1,024 to 1,048,576; twenty diagnostics per kind are retained with overflow counts. Display strings may be clipped, but identities, pointers and cursors remain intact. If required intact output cannot fit, return limit_exceeded instead of malformed JSON or shortened identity.
Metadata projections exclude raw bodies, arbitrary event/link details and run policy/environment blobs. Small declared conditions appear only outside no_content; names and labels remain untrusted data. no_content is an output policy, not a confidentiality guarantee. Explicit content routes are payload and SQL.
Continuation resumes after the last emitted record, including byte-based trimming; oversized cursors are refused at 8,192 characters. Pages are independent live snapshots. Supported comparisons require complete observed originals in the same run and projection; incompatible/missing tokens yield null plus a reason. A validated binding and an available equality comparison are distinct facts.
Read operations and envelope construction share a cooperative deadline. Expected refusal/storage/deadline errors use safe JSON envelopes; unexpected implementation or process-control exceptions are not silently reclassified. Cleanup removes reader hooks, clears cache/prefix state, rolls back and closes owned connections before outcome logging.
Python and CLI use the same mfr.inspect/1 envelope, metadata projections, validation order and error codes. Each run_read invocation owns a read-only connection, one short transaction and one cooperative deadline. Required complete counts finish inside that transaction; deadlines or storage failures produce errors instead of partial success claims.
For a valid run-scoped CLI command without --run, _infer_run first performs a separate run-selection read, then _scoped invokes the selected reader. These reads have separate transactions and deadlines; for check/find_turns/explain/payload/slice, the selected timeout (default 10 seconds, finite positive value up to 30) is forwarded to both. CLI validation precedes run selection. This is a per-read budget, not an end-to-end deadline. With an explicit --run, only the selected reader is needed. fr schema and Python schema() return static metadata without opening a recording. Snapshot has its own connection and deadline management rather than using run_read.
One Context-owned _ReadCache retains immutable facts within that read snapshot: at most 8 MiB accounted bytes, 4,096 entries and 1 MiB per entry. Run-scoped typed keys distinguish raw and observed event rows, artifacts, relations, answer descriptors and membership minima. Request manifests and oversized entries bypass admission. Output arrays, SQLite buffers and process RSS are separate. Every lookup checks the deadline, including cached misses; each later public call starts fresh.
Stable answer descriptors exclude current-request completeness. One previous-request scalar slot per generator supplies that occurrence-specific fact; comparison precedence, row order, repeated diagnostics and overflow counts remain unchanged. A streamed, guarded membership prefix is only a negative prefilter: earlier minima still use original manifest authority. Any minimum eviction/refusal permanently disables it for that invocation; interrupted fills cannot advance coverage. Cache state is cleared before the owned read returns.
The private MEMBERS_SQL relation projects message_path and the message-pointer prefix with direct json_extract(a.body_text, '$.message_path'), while its predicate requires json_valid(a.body_text). The json_each iterator and format check retain their guarded input expressions, so malformed or omitted manifest bodies produce no members. Valid paths keep their SQLite scalar types; ordering, duplicate occurrences and per-occurrence diagnostics remain intact. This shared relation serves aggregate and detailed readers; input-answer and produced-answer populations remain separate and complete.
For turn membership totals, the private _member_count traversal reads request sequence and omission reason from the same guarded member selection. It preserves row selection and order, per-row deadline checks, and display diagnostics without projecting member records that the caller would discard. check validates links before operation lifecycle checks so later operation lookups do not evict membership-prefix facts before relation validation. Public item order, witness order, repeated diagnostics and overflow counts remain unchanged.
Turn answer totals use the private _answer_summary consumer of _answer_states, the shared occurrence traversal also used by answer_rows. The traversal preserves the original SELECT *, filters and ordering, descriptor checks, current-request completeness, per-occurrence deadlines and diagnostics. answer_rows still builds the full public records; turn totals consume all states without creating unused dictionaries. The input-answer and produced-answer passes remain separate and complete.
When decision_ids is not None, _answer_states consumes that iterable once and adds a redundant semijoin to the modern occurrence arm: member artifact IDs must appear in the same run's answers links with message_binding/2 and a selected source decision. The original modern join still chooses the links and preserves their multiplicity; the outer source filter still distinguishes decisions sharing a message handle. The complete legacy UNION ALL arm, outer request/source selectors, ordered output and LIMIT remain intact. Membership still spans the whole run, so produced answers include later requests in other turns. Numbered placeholders reuse the existing run and decision parameter slots: the binding count remains one run ID, the selected request and decision IDs, and an optional limit. This statement-local SQL filter adds no retained cache and preserves every occurrence's comparison, deadline and diagnostic evaluation.
Within _turn_facts, the already-owned request_info result is passed as private witness_info through witness and relation validation for the current request. Reuse requires both run and witness-event identity to match; other witnesses use the normal request lookup. The value is not admitted to the Context cache or retained across requests.
Check aggregates and navigation/explanation totals consume their complete relevant retained populations, including occurrence comparisons and diagnostics, before presentation limits. Those completed totals remain exact for retained facts. Slice and SQL have explicitly bounded traversals/results. A deadline or storage error never promotes a partial aggregate to a successful complete total. Attribution follows explicit identities. Missing observations, invalid bindings, unsupported representations and traversal cutoffs remain visible.
fr snapshot independently probes the source read-only and creates its SQLite backup in a private same-parent staging directory. It finalizes, reopens and verifies the staged DELETE database, prepares the result and closes the source before creating any public output. A single exclusive hard link publishes the verified main without overwriting a competing file. A sidecar visibly present immediately after link success causes recording_unreadable/destination_finalize rather than a false verified-success status, and the published main may already exist; this best-effort observation is not an atomic multi-path lock. Failures/interruption remove only private staging files: public main and sidecar paths are never cleanup targets. Snapshot does not use the capture writer or write its source. It preserves source state and does not repair, resume or modify a recording. A portable copy of an incomplete run remains incomplete.
8. Operational logging
Module loggers under mcp_flightrecorder report recorder lifecycle, capture loss, writer failure, adapter evidence loss, inspection failure and snapshot outcomes. The package installs only a NullHandler; applications own levels, handlers and destinations. No recording payload, path, tool/client name, SQL, raw exception or traceback is passed to logging.
Capture has one receipt/logging boundary after preparation and admission. The recorder claims each fixed loss category once under its condition, then emits outside that lock. Repeated public close/finalize results are suppressed until the result changes. Adapters reuse the existing per-invocation loss deduplication. Readers release their transaction before logging; snapshot cleanup completes before its final outcome is reported. A shared small emitter contains ordinary handler/filter exceptions. It does not add a queue or background worker.
Internal storage verification is a DEBUG event; only the public joined-owner result reports portability. A portable but incomplete recording still produces a warning. Reader and snapshot SQLite failures use one shared translation function, preserving deadline precedence. The capture contract describes synchronous handler ownership and application configuration.
9. Source distribution
The source tree contains current library modules, short end-user examples, test-owned recording producers under tests/producers/, tests, small declarative JSON inputs, requirements and documentation. Tests exercise the installed capture, inspection and snapshot implementations. Independent fixture expectations and canonical SQL assertions live in the normal pytest suite, with six plain shared functions in tests/fixture_checks.py. Tests generate their SQLite fixtures under temporary directories. Generated recordings, logs, reports, experiment outputs, dependency wheels, caches and historical handoffs are excluded. Testing describes the separate dependency profiles.
A committed tree is exported into a pristine directory and checked against tools/public_files.txt. A separate build copy produces the wheel and sdist; tools/check_package.py validates their actual members, metadata, license files and schema against that source. Temporary negative controls exercise refusals without entering the repository. Fresh virtual environments install the wheel normally, and smoke checks verify the import origin before using the CLI and public API outside the source tree. A fresh sdist extraction is rebuilt and installed to check source completeness.
The read-only GitHub Actions workflow runs the 19-module SDK-free selection on Linux CPython 3.11 and 3.14, plus the separate ten-module main integration selection on CPython 3.14. Each suite has its own environment and four workers. The core 3.11 job also rebuilds and installs the sdist. These are configured checks, not a claim of observed hosted success; the workflow does not publish artifacts or packages. Testing documents the commands and limits.
Staged publication
The manual publish.yml workflow admits main-only validate-only or explicit staged-release dispatch with a full reviewed commit, version 0.5.16 and planned tag 0.5.16. Staged release additionally requires that bare tag to exist as an annotated tag at the approved commit. A clean source export builds one original wheel/sdist pair, with package guards, strict Twine, installed smoke and an sdist-rebuilt wheel comparison. Both modes store a flat candidate containing the pair, manifest.json and SHA256SUMS; source/workflow identity, exact main-push CI and artifact hashes remain bound to the run.
Validate-only ends after candidate storage. Staged publication promotes the same bytes through the testpypi, pypi and github-release environments. A release reviewer inspects this run's candidate before TestPyPI approval; PyPI follows TestPyPI download/install verification and a second environment approval. GitHub draft/published-state verification follows PyPI download/install verification. Identity is rechecked after waits. Only proven identical existing files permit partial resume; conflicts stop without deleting, replacing or retagging public state. These are configured release paths, not an observed publication claim. Releasing contains exact setup and operator steps.
Configured release flow; all staged jobs use the same four-file candidate.
10. Integration boundaries
Model facades bind the selected native signature at invocation; MCP facades expose fixed method signatures. Each invocation claims a fresh ModelEvidence or OperationEvidence and fixes scope before returning deferred work. Native MCP argument validation still occurs when the native method executes. Wrong/reused evidence is a caller error before native execution. These objects hold receipts and bounded losses rather than duplicate payloads. Projection and policy happen at execution; never-awaited work creates no observation, native call or reservation. M04 and M05 display the two lifecycles.
MCP attribution is an accepted observed dispatch scope (host_hook), otherwise an explicit valid decision receipt (propagated), otherwise none. Invalid attribution records loss rather than guessing. Default model role=tool annotations remain unresolved (binding_not_supplied) even when a provider tool-call ID is present. Explicit host annotations/insertions supply ancestry.
MCP native results/errors distinguish ok, tool error, protocol error, timeout, transport error, cancellation and local error. Only an admitted start with a ticket permits a terminal attempt. Facades retain resolution="unknown" and input_rounds=None; cache options do not prove hits. They do not observe wire request IDs or split SDK-internal retries, sampling, elicitation or progress into independent events. Host-observed cache use enters the explicit core API.
The stream accumulator has two independent 1,048,576-byte pools for encoded raw chunk projections and pending tool ID/name/argument fragments across all keys, with at most 64 choices and 64 lifetime tool keys. Emission or unsupported-tool discard releases only fragment bytes; emitted keys still occupy their lifetime positions. Raw overflow closes body retention and makes the aggregate partial while selection inspection continues on the overflow chunk and later host-consumed chunks. Later full raw projections are not serialized again, but trusted projection, validation and fragment admission continue. A fully observed tool can remain complete and emit before its finishing native chunk is returned. Fragment/cardinality overflow keeps the conservative selection stop. Actual projection gaps mark pending and subsequent tool fragments partial; already-emitted decisions are not revoked, and later retained raw observations need not be a contiguous native prefix. At ordinary finalization, remaining supported or provisional tool occurrences emit incomplete decisions; explicitly unsupported tool types are discarded. Finalization clears both pools and their counters. The combined bound is 2,097,152 retained encoded buffer bytes, plus bounded container/object overhead. It does not bound native chunk size, transient projections, assembled payloads, all capture memory, callback time or process RSS.
The host drives every read and cleanup. EOF is complete only with finished choices and no partial-source condition; native error/cancellation and early close retain their own states. Early close forwards native cleanup and records the consumed aggregate as partial, error or cancelled according to the cleanup outcome, without draining. Abandoned streams receive no automatic cleanup. Ordinary terminal observation is guarded once; policy process-control interruption has the limits documented in §6.
Retention is selected separately for each request, decision and response/consumed aggregate. Redacting a decision does not redact its other representation in the response or stream aggregate. Default None or unavailable MISSING performs no callback. A configured callback gets detached values; its returned mapping is detached and validated. Ordinary callback/validation failures, including callback-raised cancellation, select fixed omission plus one bounded loss. Other process-control exceptions propagate. Metadata capture overrides body retention. Full supported signatures remain in Integrations.