OpenProgram / Provider design / implementation handoff
Request recording and strict offline replay
Design audit baseline: 990bfe36e625579443e1cb01a1b3266c8dbd0e87; lifecycle implementation review: 99252205957a2850e1203a888a0fa0b403b0c3f7. This page defines product entry points, the provider runtime lifecycle, registry wrappers, file safety, strict offline behavior, and verification contracts, with separate design and implementation evidence.
RecordingProvider, ReplayProvider and registry transform; add an explicit, thread-safe, process-wide provider runtime lifecycle. Importing openprogram.providers does not register vendor providers, read record_replay settings, open recording files, or install a transform. Registration and activation finish before the first actual provider lookup; openprogram recordings performs management operations only, without entering the runtime lifecycle. Recording is disabled by default; redaction cannot be disabled. Replay never reads credentials, delegates to a live provider, or uses provider fallback.
01 · Goals, non-goals, and security boundaries
Goals
- Enable complete provider request recording through the product CLI/config.
- Reproduce multiple provider calls across APIs offline from one file.
- Write files safely under concurrency, with owner-only permissions and mandatory redaction.
- Provide scriptable list/show/delete/prune operations.
- Identify errors by file, line, call, event, or field.
- Separate provider registration, runtime activation, and recording management into independently callable and verifiable boundaries.
Non-goals
- Do not record tools, MCP, browser operations, or arbitrary HTTP traffic.
- Do not define, persist, or restore workflows, Agent DAGs, Sessions, or tool side effects; replay replaces only the LLM provider request/event boundary.
- Do not define JSONL as a permanently stable public interchange format.
- Do not perform fuzzy matching across models or prompts.
- No Web UI entry point in this phase; CLI/config cover the worker first.
- Do not automatically upload, share, or commit recording files.
- Do not rewrite the provider plugin protocol, change the recording format, or support runtime hot switching.
Security boundaries
- “Strict offline” guarantees no network access only at the LLM provider layer.
- Tools still run under their own permissions during replay; use sandbox/network policy when the entire process must be offline.
- Redaction handles credentials only; prompts, responses, and tool results may contain private business data.
showshows metadata by default; content requires an explicit request.
02 · Current baseline implementation and gaps
| Capability | Current evidence | Status | Gap |
|---|---|---|---|
| Recording, replay, and redaction | recording.py, replay.py | Implemented | Tests cover strict v1 JSONL, concurrent writes, mandatory redaction, offline mismatches, and the legacy reader. |
| Registry transform | api_registry.py::configure_provider_transform() | Implemented | Covers existing and subsequently registered providers; permits only one transform per process. |
| CLI/config and file management | openprogram recordings | Implemented | status/record/replay/off/list/show/delete/prune and next-start settings are implemented. |
| Management-command recovery | e8c9f645 | Implemented | Recording management does not initialize the provider runtime; recovery from a corrupt replay file no longer requires an environment-variable sentinel. |
| Provider runtime lifecycle | initialization.py, register.py | Implemented | Verified explicit NEW/INITIALIZING/READY/FAILED states, concurrent first initialization, redacted failures, atomic auth/API publication, and startup fail-fast behavior. |
| Concise Markdown reference | record-replay.md/.zh.md | Synchronized | Documents the current CLI, strict offline guarantees, and lifecycle implementation evidence. |
providers/__init__.py does not register built-ins, read settings, open recording files, or install a transform. Public provider lookup and Web/worker startup explicitly call initialize_provider_runtime(); recording management operates directly on settings and files, without an environment-variable protocol.03 · Corresponding design in the reference corpus
The repository reference snapshot contains only references/pi-ai. This section describes that snapshot alone, without claiming coverage of other versions or the entire open-source ecosystem.
| Object | Actual mechanism | Recording/replay support | OpenProgram decision |
|---|---|---|---|
references/pi-ai/src/stream.ts |
stream()/streamSimple() resolves a provider by model.api and calls its methods directly. |
Absent from the snapshot. The snapshot has no recording/replay module, configuration entry, or recording-file management; stream.ts also has no wrapper hook. |
Adopt one registry dispatch point; transform providers centrally in the registry instead of editing each vendor module. |
references/pi-ai/src/types.ts |
Shares Model, Context, stream options, and event types. |
Absent from the snapshot. No recording manifest or replay mismatch type exists. | Serialize the existing corresponding Pydantic types; do not create a second request/event domain model. |
Rejected approaches: per-vendor recorders, raw HTTP proxy capture, and test mocks as substitutes for product entry points. These duplicate implementation, expose unredacted wire data, or fail to cover actual worker configuration.
04 · Architecture
Retain
Existing JSONL, strict parser, shared sink/replay, registry transform, next-start settings, management functions, and direct RecordingProvider/ReplayProvider construction in the library API.
Separate
register_builtins() handles provider/auth adapter registration only; initialize_provider_runtime() owns the once-only configuration snapshot and transform. Management commands call neither. They can diagnose corrupt settings or replay files and disable them. Management changes take effect on the next start and do not reset the current process lifecycle.
Add
A shared lifecycle state, concurrent first initialization, cached failures, mandatory initialization before public provider lookup, and explicit startup fail-fast calls in Web/worker.
Reject
No environment-controlled import, per-vendor initialization branches, configuration hot reload, format changes, or provider-runtime dependency in recording management.
05 · CLI and configuration contracts
CLI syntax
openprogram recordings status [--json] openprogram recordings record [--name NAME] openprogram recordings replay <ID-or-PATH> openprogram recordings off openprogram recordings list [--json] openprogram recordings show <ID-or-PATH> [--json] [--content] openprogram recordings delete <ID> [--yes] openprogram recordings prune --older-than-days N [--dry-run] [--yes]
| Command | Defined behavior | Exit code |
|---|---|---|
status | Show effective mode, selected file, configuration source, file availability, and restart requirement. Do not read recording content. | 0 when valid; 1 for invalid configuration. |
record | Create and validate a managed ID; precreate a header-only file with mode 0600, then atomically write mode=record and the selector. If any step fails, remove the newly created empty recording and preserve old settings. Do not restart the worker immediately. When no name is supplied, generate a UTC timestamp with a random suffix. | 0 only after both file and configuration succeed. |
replay | Strictly parse the selected file first; on success, atomically write mode=replay and the selector. | 1 for an invalid file; configuration remains unchanged. |
off | Set mode to off only; retain the last selector for explicit reuse. | 0 on success. |
list/show/delete/prune | See §10. Management commands do not change mode. | 2 for argument errors; 1 for operational errors. |
Configuration schema
{
"record_replay": {
"mode": "off", // off | record | replay
"file": "" // managed ID or explicit filesystem path
}
}
| SettingSpec | Type and default | apply | Validation |
|---|---|---|---|
record_replay.mode | enum, default off | next_start | Only off/record/replay. |
record_replay.file | text, empty by default | next_start | May be empty when off; empty record selection fails at startup (CLI record generates it first); replay requires a strictly readable file. |
openprogram config set can still edit each setting separately; the recommended recordings record/replay/off uses setup.update_config() to write both fields at once, avoiding intermediate states. Settings are read only when initialize_provider_runtime() moves from NEW to INITIALIZING, never after READY or FAILED. Changes require restarting the affected process, which command output must state explicitly. Management commands inspect/update settings without runtime initialization.
06 · Registry wrappers and lifecycle
Import contract
import openprogram.providers only defines Python APIs, exports model APIs, and loads existing static model modules. It must not call register_builtins(), read record_replay settings, open recording files, install a transform, or mutate the credential registry through auth-adapter imports. Isolated subprocess tests verify this contract; comments are insufficient. Removing other existing read-only initialization in model catalog modules is outside this task.
Responsibilities and interfaces
| Interface | Responsibility | Prohibited behavior |
|---|---|---|
register_builtins() | Resolve built-in providers idempotently and thread-safely; publish API providers in one atomic registry batch; explicitly load existing auth adapters. | Do not read record/replay settings, access recording files, or install transforms. Do not silently treat internal import errors as missing optional SDKs. |
initialize_provider_runtime() | Serialize built-in registration, configuration snapshotting, record/replay preconstruction, and transform publication. | Do not resolve credentials, construct vendor clients, or make network requests. |
get_api_provider(api) | At the public provider lookup boundary, ensure runtime READY before returning the final registry provider. | When FAILED, do not return the original provider or bypass recording/replay settings. |
create_runtime(...) | Ensure provider runtime READY first; off/record retain existing routing, while replay builds a base Runtime from an explicit or non-credential default provider/model. | In replay, do not perform CLI/AuthStore detection or construct a subscription runtime that reads credentials. |
| recordings management | Read/validate files and update next-start settings. | Do not call initialize_provider_runtime() or depend on the provider registry. |
api_registry retains register_api_provider(api, provider), get_api_provider(api) and a once-only transform. The transform covers registrations made both before and after initialization.
configure_provider_transform(transform) - Set once per process; the same value is a no-op, conflicts raise RuntimeError - Retain the original provider; do not wrap an already transformed provider again - Atomically replace all current registry values - Later register_api_provider(api, original) stores transform(api, original) automatically record: shared_sink = RecordingSink(path) transform(api, original) -> RecordingProvider(original, shared_sink) replay: shared_replay = ReplayProvider(path) transform(api, original) -> shared_replay original only confirms that the API is registered; ReplayProvider neither retains it nor delegates to it
All recording wrappers share one sink; all replay APIs share one ReplayProvider, so call_index represents the total order of provider API calls in the process, rather than a separate sequence starting at 0 for each API. Direct library use still supports independent constructors; product activation must not require callers to enumerate APIs.
Process-wide state
NEW
└─ initialize_provider_runtime() ─→ INITIALIZING
├─ success ─→ READY
└─ any incomplete exit ─→ FAILED (immutable failure descriptor)
- State and waiting use one
threading.Condition; one thread initializes at a time, while others wait for READY or FAILED. - READY never rereads settings; changes continue to follow
next_start. - FAILED occurs only during built-in registration: the runtime stores the stage, cause type, and redacted cause message. Each caller receives a fresh
ProviderRuntimeInitializationErrorwith identical fields and message; do not reuse exception objects carrying old tracebacks. - Record/replay activation failure does not set runtime FAILED: log the error, atomically replace the registry transform with a fail-closed
_BlockedRecordReplayProvider, and enter READY withsnapshot.mode == "blocked". Every provider call raises the same diagnostic, directing the user toopenprogram recordings statusoropenprogram recordings off; no credential resolution or network access occurs. KeyboardInterrupt/SystemExitand similar control exceptions store ainitialization_interruptedfailure descriptor under the lock andnotify_all(); the initializing thread rethrows the original control exception, while waiters and later callers receive a stable initialization failure. No in-process retry; restart begins at NEW.- Construct
RecordingSink/ReplayProviderand complete validation before publishing the transform; publication failure must not leave a partially replaced registry. - Use fresh subprocesses to isolate off/record/replay/blocked/FAILED tests; add no production API that resets global lifecycle state.
Visibility of built-in registration
register_builtins() imports and validates built-in modules under its own lock, builds a local API-provider batch, and publishes it once through the registry. Failure before publication leaves the API registry unchanged; mark registered only after complete success. Anthropic/Codex/Gemini auth adapters retain their idempotent registration functions, moved from package import to this explicit phase. If an auth adapter fails, the provider runtime enters FAILED; public get_api_provider() cannot observe or use published providers because of the lifecycle gate. A process restart rebuilds them. Anthropic/OpenAI/Google SDKs are core dependencies, so wrapping the entire provider import in except ImportError must no longer hide internal defects.
Explicit startup and shared guarantees
Web and worker explicitly call initialize_provider_runtime() before starting recovery threads, dispatchers, or provider warm-up threads, so invalid replay is reported during single-threaded startup. Public get_api_provider() still performs an idempotent check, covering Python library calls and future entry points. Lazy auth-adapter registration calls only register_builtins(); a corrupt replay file cannot cause it to fail.
Runtime construction and credential boundary
Strict offline behavior must cover create_runtime(), not just stream.py. The current factory and subscription constructors read API keys/OAuth before get_api_provider(). The target does not refactor live runtimes: off/record retain the existing factory; replay adds a credential-free branch accepting explicit provider/model choices, or AGENTIC_PROVIDER/AGENTIC_MODEL and the existing non-credential selection in configuration default_provider/default_model. Without a selection, immediately return a replay configuration error; do not proceed to CLI or AuthStore detection.
The replay factory does not construct a subscription runtime. It uses pure provider-to-model-namespace metadata from PROVIDERS to construct a base Runtime. Plain providers already have this field; add model_namespace: openai-codex→openai-codex, gemini-cli→gemini-subscription to the subscription entries. API-routed providers retain their model-catalog namespace. This branch does not infer the original factory provider from the recording: v1 requests store only the wire-level Model, which does not encode the original runtime factory choice. Nor can the first model in a multi-API file define all subsequent runtime identities. Once a request enters ReplayProvider, existing full request comparison validates the final Model, context, tools, and options; mismatches use the existing ReplayMismatch diagnostic.
Relationship to the OpenProgram execution model
record/replay supports provider testing and failure reproduction, not workflow definition. Agent loops, Runtime retries, tools, Session persistence, and DAG state execute normally; only the registry provider changes from live/record to ReplayProvider. Replay can therefore verify upper-layer behavior against the same model-event sequence, but it does not restore historical Sessions, replay tool side effects, roll back files, or define execution steps.
07 · Recording files, strict parsing, and concurrent writes
Format policy
New writers continue to emit format_version=1 and the existing four line types.call_end.outcome is optional diagnostic metadata: absence means complete, preserving compatibility with old v1 files and readers. It does not change event models or request matching. Headers may gain optional metadata; v1 readers must ignore unknown header fields. Bump the version only when required request/event fields or semantics change.
{"type":"header","format_version":1,
"recording_id":"20260811T134502Z-f7a29c1d",
"created_at":"2026-08-11T13:45:02Z","redaction_version":1}
{"type":"request","call_index":0,"model":{...},"context":{...},"options":{...}}
{"type":"event","call_index":0,"event_index":0,"event":{...}}
{"type":"call_end","call_index":0,"event_count":1,"outcome":"complete"}
recording_id, created_at and redaction_version support management and diagnostics only, not request comparison. Old v1 files without these fields remain replayable; list/show derive missing information from stat and content.
Strict-reader invariants
File level
- The first nonempty line must be the only header.
- Every line must be a JSON object; errors include the line number.
format_versionmust be a supported integer.- Accept only header/request/event/call_end.
Call level
- Each request has a unique
call_index, consecutive from 0. - Events must reference an existing, unfinished call.
event_indexis consecutive from 0.- Each request has exactly one call_end.
Completion level
event_countequals the actual event count.outcomeaccepts only complete/error/cancelled/abandoned; missing values default to complete for compatibility.- No more events for a call after call_end.
- Unknown event types fail during provider construction, not midway through replay.
- Reject empty files, truncated final lines, and orphan lines.
Request level
- Apply the same redaction version to incoming requests before comparison.
- Compare model/context/options in that order.
- Preserve call, field, recorded, and incoming in differences.
- Reject calls beyond the recording; never fall back to a live provider.
Concurrent-write contract
RecordingSinkholds a process-localthreading.RLock; serialize and append each JSONL line within the same critical section.- Each append opens the data file with
os.open(O_WRONLY | O_CREAT | O_APPEND, 0o600)and usesopenprogram._compat.flock(fd, LOCK_EX)for interprocess exclusion. - Create the initial header and allocate the next
call_indexunder the file lock. The minimal implementation may scan the maximum request index at begin_call: accept O(n) allocation on this diagnostic path, adding a sidecar counter only if measurements show large files are a bottleneck. - Write a complete UTF-8 JSON record plus newline using a
os.writeloop; do not assume one write completes it. - Do not force fsync after each request/event append; fsync after call_end. A crash may leave an incomplete call that the strict parser detects.
- Events may interleave across threads/processes;
call_indexfromevent_indexgroups each call completely. Replay order follows request call_index.
08 · 0700/0600, path constraints, and redaction
Filesystem
- The managed root is
get_state_dir() / "recordings"; addget_recordings_dir()and tighten it to 0700 after creation. - On POSIX, tighten the managed recording root to 0700 whether new or existing. A library-level explicit external
RecordingSink(path)does not change parent-directory permissions or follow parent symlinks to chmod. Create data files as 0600 on first write. After each open of an existing record target, callopenprogram._compat.restrict_to_user(path)to repair overly broad permissions, then verify mode 0600 withfstat; reject recording if it cannot be guaranteed. - Replay distinguishes managed and explicit external paths: managed files may be tightened to 0600; external files must already be regular files with mode 0600 and are not
chmod-modified. Explicitly reject overly broad external permissions for the owner to resolve. - Create lock files as 0600 too. list/show/delete/prune use
lstatto reject symlinks and verify managed-root containment after resolution. - Recording uses
O_NOFOLLOWwhere supported andO_EXCLto initialize a new file; never truncate an existing recording. Name conflicts fail immediately. WithoutO_NOFOLLOW, open the basename relative to a managed-root directory fd, then comparelstat/fstatdevice, inode, and regular-file type against the opened result. - delete/prune delete only regular files with the
.jsonlsuffix; no recursion, symlink following, or explicit external deletion.
Redaction contract
remove_secret_values()runs before any serialization; record mode has no disable option.- Retain the current case-insensitive sensitive-field set, including nested dicts/lists. Field names recognize
-/_and camelCase boundaries. Compound header/option names ending intoken,key,secret,passwordare fully redacted too, such asX-Session-Token,service_api_key,refreshToken,clientSecret. Do not match non-credential counters such astoken_count/tokenCount. - Value patterns cover common Authorization schemes (Bearer, Basic, Bot, token, Key), URL userinfo, and the currently implemented query keys
api_key/apikey/access_token/token; adding a query key requires implementation and tests together, not a design-only claim. - Do not use generic high-entropy-string rules: they unpredictably change normal prompts and cause replay mismatches. New provider credential forms require deterministic patterns and regression tests.
- record and replay must dispatch using the same
redaction_version. Treat existing v1 files without this field as redaction v1.
show --content must explain this.09 · Strict offline replay contract
Structural constraints enforce replay isolation; it does not depend on callers voluntarily avoiding network access.
- The replay transform returns the same
ReplayProvider. ReplayProviderretains no original provider, imports no vendor transport, and offers no fallback.stream.pymust obtain the provider first; when the provider declaresrequires_credentials = False, do not callresolve_provider_key().- Fully parse and validate the file before transform installation; corrupt files block worker startup rather than failing midway through startup.
- Unknown APIs still produce the registry missing-provider error; never dynamically load live providers for replay.
- Request mismatches, exhausted recordings, unknown events, and event-validation failures raise directly; no failure may switch to a live provider.
Tests replace both socket.socket.connect, socket.create_connection and shared HTTP-client constructors with failing sentinels, asserting no network attempt on successful replay, mismatch, corrupt-file, and exhaustion paths.
10 · Recording-file management
| Command | Default output/behavior | Safety rules |
|---|---|---|
list | ID, created_at, format version, call/event counts, integrity, bytes, and active marker; newest first.--json emits stable fields. | A corrupt file shows invalid and a brief error without blocking other entries; no request content is printed. |
show | By default, parse and show header, statistics, each call’s model provider/api/id, event count, and completion status.--content is required to print the complete redacted JSONL. | Print a risk notice to stderr before content reaches stdout; do not mix notices into JSON output. |
delete | Delete one managed ID. A TTY requires typing the complete ID by default; without a TTY or --yes, exit 2. | Reject active selectors, external paths, directories, and symlinks; deletion is irreversible. |
prune | Delete managed files older than N days using stat mtime; --dry-run lists candidates only. Output matched/deleted/failed/bytes. | N must be a positive integer; skip active files, invalid symlinks, and files holding an exclusive lock. Without --yes, request TTY confirmation or exit 2 in non-TTY mode. |
list/show use the same pure inspect_recording(path) function, and replay activation uses the same strict parser. Do not add a permissive management parser. prune uses stat mtime for age; header created_at is display-only, preventing header edits from changing retention.
11 · Failure semantics
| Phase | Condition | Exception/CLI behavior | May continue? |
|---|---|---|---|
| Built-in registration | Vendor import failure, auth-adapter registration failure, or aborted batch publication | Runtime enters FAILED and stores an immutable failure descriptor (stage="builtins"); each caller receives a fresh ProviderRuntimeInitializationError with consistent fields; wake every waiter; never return the original provider. | Management commands may continue |
| Record/replay activation | Unknown mode, empty file, external record path, or missing/corrupt/unsupported replay file | Structured error log; atomically replace the registry transform with a fail-closed _BlockedRecordReplayProvider; runtime READY with snapshot.mode == "blocked". Every provider call raises the same diagnostic, directing the user to openprogram recordings status/off; no credential resolution or network access occurs. | Diagnostics only |
| Control-flow exit | Initializing thread receives KeyboardInterrupt/SystemExit | Enter FAILED and wake waiters; initializer rethrows the original control exception, other callers receive initialization_interrupted; only a process restart permits retry. | Management commands may continue |
| Creation | Existing filename, symlink, unwritable directory, or permissions that cannot be tightened | RecordingFileError; do not overwrite existing files. | No |
| Writing | Lock failure, full disk, or serialization failure | Stop the current provider call and retain written data; do not merely log and continue live execution. | No |
| Live provider | Startup/midstream failure, cancellation, or consumer closing the generator early | In finally, write call_end with outcome and close the source generator, then preserve the original exception/cancellation semantics. The whole file remains parseable. Replay validates the request first and reports interruption only when that call is consumed; counting then allows retry against the next recorded call. | Per call |
| Parsing | Invalid JSON, version, ordering, count, or event type | RecordingFileError(path, line, call_index, reason). | No |
| Matching | Incoming differs from recorded | Retain existing ReplayMismatch fields; CLI/Web error chains show call and field. | No |
| Exhaustion | Incoming call exceeds recorded calls | ReplayMismatch(field_path="call_index"). | No |
| Underconsumption | Process/test ends with unconsumed recorded calls | Retain assert_consumed(); one-shot/program/test entry points call it at normal completion and fail if needed. A long-lived worker shows consumed/total in status without declaring an error prematurely. | Per entry point |
| Management deletion | Some file deletions fail | Report each file; prune continues with the remainder, then exits 1. | Yes |
12 · Compatibility and migration rules
- Library API:
RecordingProvider(provider, path),ReplayProvider(path),recording_path,call_countand existing exception attributes remain. - Existing v1 files: Continue reading them; stricter checks reject truncated, orphaned, or duplicate lines previously ignored by permissive readers. This fail-closed behavior is intentional.
- Old writers: Headers containing only type/version remain valid; derive management metadata from stat/content and mark it
legacy-v1. - Default execution:
mode=offtakes one configuration snapshot on first actual provider lookup; later requests add no config-file I/O. Provider identity and credential-resolution order remain unchanged. - Configuration: A missing
record_replaysubtree means off. Environment variables do not automatically enable it, and no scan selects the “latest recording.” - Import:
import openprogram.providersno longer guarantees a populated built-in registry; actual provider lookup and explicitregister_builtins()take that responsibility. Public stream/complete behavior is unchanged. - Management recovery: Remove
_OPENPROGRAM_RECORDINGS_MANAGEMENT; structural separation guarantees status/off recovery, independent of import order. - Markdown:
record-replay.md/.zh.mdtracks this page’s current CLI, strict offline guarantees, and lifecycle boundaries, replacing the obsolete test-library-only description. - Future formats: Use separate parsers for version upgrades; never rewrite user files in place. Offer an explicit migrate command only for defined field-semantic changes.
13 · Minimal implementation files
| File | Minimal change | Reason for reuse |
|---|---|---|
openprogram/providers/initialization.py | Add runtime state, Condition, once-only built-in registration, configuration snapshot, transform activation, and failure propagation. | Lifecycle coordination stays separate from recording format and registry storage. |
openprogram/providers/register.py | Make register_builtins() thread-safe and mark success only after complete execution. | Keep the existing vendor-registration list; do not duplicate it in the lifecycle module. |
openprogram/providers/api_registry.py | get_api_provider() ensures runtime READY before registry lookup; retain existing atomic transform publication. | The common boundary for every actual API-provider lookup. |
openprogram/providers/registry.py | Add pure model_namespace metadata to the subscription entries; create_runtime() obtains a lifecycle snapshot first and adds a credential-free base Runtime branch only for replay. | Retain live factory, subscription runtime, and credential behavior for off/record. |
openprogram/providers/__init__.py | Remove built-in registration and record/replay activation calls; retain exports only. | Eliminate package-import effects on runtime state and the filesystem. |
openprogram/providers/recording.py | Keep format and management functions; invoke activation explicitly through the lifecycle, no longer during import. | Do not relocate stable recording/replay logic. |
openprogram/auth/credential_provider.py | Replace side-effect provider imports with explicit register_builtins(). | Auth needs adapter registration only, not replay activation. |
apps/server/openprogram_server/server.py | Explicitly initialize provider runtime before starting threads. | Preserve single-threaded preloading and expose its failure boundary as an explicit API. |
openprogram/worker/runner.py | In start_web(), provider warm-up, and channel threads are preceded by explicit initialization. | Worker startup consistently fails fast on invalid replay configuration; background threads may not initiate first initialization independently. |
openprogram/cli/__init__.py | Remove the recording environment-variable sentinel; keep direct management-function dispatch. | Module boundaries guarantee management recovery. |
tests/component/providers/registry/test_provider_initialization.py | Add import-purity, state-machine, concurrency, failure-atomicity, and auth/Web boundary tests. | Separate lifecycle tests from format and CLI tests. |
tests/component/providers/registry/test_record_replay_cli.py | Add real subprocess recovery cases for status/off with missing, corrupt, and unsupported-version files. | Verify user recovery entries; do not substitute unit mocks for startup behavior. |
Do not change vendor providers, off/record live factories, Web routes, Node CLI, or the recording format. Add only the replay branch to the runtime factory. Calls bypassing get_api_provider() or plugins bypassing register_api_provider() must return to the shared boundary; do not add vendor-specific cases in the recorder or lifecycle module.
14 · Acceptance and test matrix
| Dimension | Case | Key assertions |
|---|---|---|
| Import contract | An isolated subprocess executes only import openprogram.providers | No config read, recording open, vendor SDK/auth-adapter import, or API-registry mutation. |
| First initialization | No settings / mode off / record / replay | Process built-ins and settings once; after READY, preserve provider identity, credential-resolver counts, and existing behavior. |
| Concurrent initialization | Multiple threads first call get_api_provider() | Only one initializer; others receive the same READY provider or FAILED cause, with no deadlock. |
| Failure atomicity | Inject exceptions in built-in registration, config reading, ReplayProvider construction, and transform publication | No original provider returned, credential lookup, or network-client construction; no partially published transform. |
| Entry-point consistency | Web startup, worker, public stream, direct library lookup, and auth-adapter loading | Actual provider use ensures READY; Web/worker fail fast before threads; auth registers built-ins without replay activation. |
| Registry | Registration before/after wrapping, repeated configuration, two APIs | Transform existing and later entries; reject conflicting transforms; recording wrappers share a sink, replay shares call_count. |
| Concurrent threads | Two threads record multiple calls/events each | Every line is valid JSON; call_index is unique and consecutive; each event_index is consecutive; call_end counts are correct. |
| Concurrent processes | Two processes write one managed file | One header, no partial lines or duplicate call_index; strict parsing passes. |
| Errors and cancellation | Provider exception, task cancellation, consumer aclose(), synchronous startup failure | Preserve control flow; close the source generator; write correct call_end outcome/event_count; preserve whole-file parseability; replay fails at the matching call without corrupting later counts. |
| Activation failure | Missing/corrupt replay file, unknown mode, invalid record selector | Application initialization does not raise; runtime READY, snapshot.mode == "blocked"; provider calls fail closed without credentials/network, and management commands can recover. |
| Replay counting | A request mismatches, then is corrected and retried | Mismatch does not increment call_count; compare the corrected request against the same recorded call. Only success or a recorded interruption consumes it. |
| Permissions | New directories/files/locks, existing broad-permission directories, managed legacy 0644 files, explicit external 0644 files | POSIX 0700/0600; record and managed replay may tighten permissions; external replay validates and rejects broad permissions without chmod. |
| Paths | ../, absolute record paths, symlinks, directories, external deletion | record/delete/prune reject escapes; replay/show may read explicit external files. |
| Redaction | Case-varied fields, nested values, compound token/key/secret/password suffixes, common auth schemes, URL userinfo/query, ordinary text | Credential bytes never appear in files; non-credential text such as token_count remains; record/replay use the same rule version. |
| Strict format | Empty file, invalid JSON, duplicate header, unknown line, orphan event, index gap, duplicate end, wrong count, unknown event | Report path/line/call/reason when constructing ReplayProvider. |
| Strict offline behavior | Successful, mismatch, exhaustion, and corrupt-file replay paths | No socket/HTTP-client sentinel or credential-resolver call; no live fallback. |
| Runtime factory | Explicit provider/model, non-credential defaults, missing defaults, plain/subscription providers | Replay uses the entry’s model_namespace without importing/constructing runtime_class, CLI/AuthStore detection, or credential resolution; ReplayProvider still strictly compares the final wire-level Model. |
| CLI configuration | record/replay/off/status, invalid selectors, concurrent config writes | Atomic update; no config change on failure; restart notice; profile isolation; no runtime initialization. |
| Recovery commands | status/off with missing, corrupt, or unsupported-version replay files | Real subprocess starts; status reports a stable diagnostic; off updates settings; no environment sentinel required. |
| Management | mixed valid/invalid list, metadata show, content show, active delete, dry-run prune, partial failure | Stable JSON schema; content hidden by default; active files protected; correct counts and exit codes. |
| Legacy files | Header-only v1 metadata generated by current tests | Valid files still replay; structurally corrupt legacy files fail new strict checks. |
| Existing loop | Multi-turn tool-loop record → replay | Event types, tool execution, and final message match current tests. |
Minimum post-implementation verification commands
pytest -q tests/component/providers/registry/test_provider_initialization.py tests/component/providers/registry/test_record_replay.py tests/component/providers/registry/test_record_replay_registry.py tests/component/providers/registry/test_record_replay_cli.py pytest -q tests/component/providers/adapters/test_scripted_provider.py tests/component/providers/registry/test_registry_from_config.py tests/unit/runtime/test_usage_stream_chokepoint.py tests/unit/providers/auth/test_auth_methods.py tests/unit/webui/test_web_config_schema.py tests/component/config/test_config_write_safety.py python -m openprogram recordings --help python -m openprogram config get record_replay.mode python -m scripts.docs_site.build python -m scripts.docs_site.checklinks git diff --check
15 · Implementation and verification record
RecordingSink writes one v1 JSONL with thread locks, interprocess flock, complete os.write and call_end, and fsync; extends mandatory redaction and enforces 0700/0600 plus symlink/regular-file checks.recording.py::RecordingSink, paths.py::get_recordings_dir; a three-process spawn test strictly parses 15 consecutive calls.api_registry.py::configure_provider_transform, recording.py::activate_record_replay_from_config, test_record_replay_registry.py.replay.py::read_recording_file, ReplayProvider.requires_credentials, stream.py; structural-failure, credential-sentinel, and multi-turn tool-loop tests.record_replay.mode/file are next_start settings. CLI provides status/record/replay/off/list/show/delete/prune. Failed configuration rolls back new files; destructive commands protect active files, symlinks, external paths, locks, and non-TTY confirmation.config_schema.py, cli/__init__.py, recording.py and CLI/management tests.status/off directly recovers configuration. The environment-variable sentinel has been removed.e8c9f645, test_off_recovers_from_a_replay_file_that_became_invalid and import-side-effect subprocess tests.snapshot.mode="blocked"), with auth/recording/runtime separation and read-only model resolution in replay.4d82d485..99252205; specification re-review PASS, quality re-review PASS; committed state tests/unit + tests/providers is recorded in the delivery report.c26df53a, a4f27b3b, fe727546, 6595d444; public-entry RED/GREEN, specification re-review PASS, quality re-review PASS.test_record_replay.py continues to cover library recording, two-turn tool loops, version rejection, and field differences.openprogram.programs.workflow.test_framework; this change does not modify that entry.Design sources: current OpenProgram provider implementation, CLI/config entries, tests, and the in-repository references/pi-ai snapshot. Implementation-status updates must cite code symbols, tests, or commits; a written design is not an implemented capability.