Release 0.20.0
Added
-
Pluggable
OverridesStoreinterface (sync alignment, CRITICAL #1) — Newapcore.sys_modules.overridesmodule exposes theOverridesStoreProtocol with defaultInMemoryOverridesStoreandFileOverridesStore(atomic YAML write via tempfile +os.replace) implementations, mirroring TypeScript'sapcore-typescript/src/sys-modules/overrides.ts(OverridesStore/InMemoryOverridesStore/FileOverridesStore).register_sys_modules(..., overrides_store=...)accepts anyOverridesStore; loaded overrides are applied toConfig(and the liveToggleStatefortoggle.*keys) at startup, andUpdateConfigModule/ToggleFeatureModulepersist back through the store on every successful mutation. The legacysys_modules.control.overrides_pathconfig key is retained as a backwards-compat shim that auto-constructs aFileOverridesStore. The previously-private_load_overrideshelper now delegates toFileOverridesStorefor symmetry. New top-level exports:OverridesStore,InMemoryOverridesStore,FileOverridesStore. -
Public
SubscriberFactoryAPI (Issue #36) —apcore.events.register_subscriber_factory(type_name, factory)andapcore.events.create_subscriber_from_config(config)(also re-exported fromapcore) bring Python to parity with TypeScript'screateSubscriberFromConfig/registerSubscriberFactoryand Rust'screate_subscriber/register_factory. Built-in factory types (webhook,a2a,file,stdout,filter) are auto-registered on import. The previously-private_create_subscriberhelper remains for back-compat. -
Pipeline
StepMiddleware(Issue #33 §2.2) — Formal middleware mechanism for pipeline steps. NewStepMiddlewareProtocol exposes optionalbefore_step(step_name, state),after_step(step_name, state, result), andon_step_error(step_name, state, error)hooks; both sync and async implementations are supported (return values detected viainspect.isawaitable(), mirroring the Issue #42 asyncon_errorfix).ExecutionStrategygains astep_middlewares: list[StepMiddleware]field plus anadd_step_middleware()registration method.before_stepandon_step_errorrun in registration order;after_stepruns in reverse (onion semantics). A non-Nonereturn fromon_step_erroris treated as a recoveryStepResultand execution continues normally; returningNonelets the original exception propagate. Exported fromapcoreasStepMiddleware. -
apcore.observability.batch_span_processormodule (Issue #43 §2) — Dedicated module hosting the canonicalBatchSpanProcessorandSimpleSpanProcessorimplementations, mirroring the layout of the TypeScript (src/observability/batch-span-processor.ts) and Rust (src/observability/processor.rs) SDKs. Adds a synchronousforce_flush(timeout_ms=30000) -> boolmethod that drains the queue while the processor remains alive (returnsTrueonce empty,Falseon deadline).shutdown()is now idempotent. Queue-full enqueues now log a (rate-limited)WARNINGso dropped spans surface in operator logs rather than only via thespans_droppedcounter. The classes remain re-exported fromapcore.observability.tracingandapcore.observabilityfor backward compatibility. -
Generic pluggable
StorageBackend(PROTOCOL_SPEC Issue #43 §1) — Newapcore.observability.storagemodule defines a namespacedsave / get / list / deleteProtocol and the defaultInMemoryStorageBackendimplementation, mirroring the shape ofTaskStore.ErrorHistory,MetricsCollector, andUsageCollectornow accept an optionalstorage: StorageBackend | None = Noneconstructor argument; when supplied, error entries are mirrored to the backend's"errors"namespace keyed by fingerprint, enabling cross-process persistence. External backends (Redis, SQL, S3) remain user-supplied. Exports:StorageBackend,InMemoryStorageBackendfromapcore.observabilityand the top-levelapcorepackage. -
TaskStore.list_expired(before_timestamp)(cross-language alignment D-10) — New method on theTaskStoreProtocol returning terminal-state (COMPLETED/FAILED/CANCELLED) tasks whosecompleted_atprecedesbefore_timestamp. Implemented onInMemoryTaskStore. Drives TTL-based reaper logic; non-terminal tasks are never returned. The method is REQUIRED on the Protocol — custom stores written before this release must add an implementation. -
Registry.discover_multi_class(file_path, extensions_root="extensions")(cross-language alignment D-15) — New instance method onRegistrywrapping the existing free functionapcore.registry.multi_class.discover_multi_class. The registry's configuredpre_approval_hookis forwarded to the underlying scanner so signature-verification and audit policies apply uniformly. The free function remains importable for existing callers; new code SHOULD prefer the method. -
Granular reload via
path_filterinput inReloadModule(#45.4) —Registry.discover(path_filter=...)accepts a glob string or list of patterns and only walks matching files; previously-registered modules outside the filter remain untouched. Patterns are matched (viapathlib.PurePath.match) against both the absolute file path and its path relative to each configured extension root. -
Error fingerprinting in
ErrorHistory— dedup by (error_code, top-frame hash, sanitized message template) (#43 §4). Newcompute_error_fingerprint(error, module_id)folds the deepest stack-framefile:lineno:func(basename only, for cross-machine stability) into the SHA-256 digest in addition to the existing code/module/normalized-message inputs. Long hex runs (≥ 8 chars) are now collapsed to<HEX>alongside the existing UUID/timestamp/integer placeholders. Legacy 3-argcompute_fingerprintretained. -
Configurable redaction via
obs.redaction.regex_patternsandobs.redaction.sensitive_keysConfig keys (#43 §5). Newobsnamespace ships with sensible defaults (password,secret,token,api_key,authorization,cookie,_secret_*, …); operators can override viaapcore.yaml.RedactionConfig.from_config(config)/RedactionConfig.default()build the runtime config;_secret_prefix matching becomes a default entry rather than a hard-coded rule. Field-name match is case-insensitive substring with-/_/space normalization (so"X-API-Key"matches"api_key"); value-regex match is case-insensitive.apcore.utils.redaction.redact_sensitiveaccepts new keyword overrides (sensitive_keys,regex_patterns,replacement).
Changed
- Event names normalized to
apcore.<subsystem>.<event>form (#36) — Four legacy event types (module_registered,module_unregistered,error_threshold_exceeded,latency_threshold_exceeded) now also emit canonical aliasesapcore.registry.module_registered,apcore.registry.module_unregistered,apcore.health.error_threshold_exceeded,apcore.health.latency_threshold_exceeded. Both forms are emitted during the deprecation window so existing subscribers keep working; the legacy emission carriesdeprecated: trueindata. Glob subscribers usingapcore.registry.*andapcore.health.*now match correctly. Deprecation: legacy bare names will be removed in v0.22.0. - Contextual auditing for system control modules (Issue #45.2) — Audit events emitted by
system.control.update_config(apcore.config.updated),system.control.toggle_feature(apcore.module.toggled), andsystem.control.reload_module(apcore.module.reloaded) now include the requester'scaller_idfromcontext.caller_id(defaults to the@externalsentinel when unset) and a redactedidentitydict (id,type,roles) whencontext.identityis present. - Pipeline configuration is fail-fast (Issue #33 §1.2) —
build_strategy_from_confignow raisesConfigurationError(new typed error, codePIPELINE_CONFIGURATION_ERROR) instead of logging a warning when YAML refers to a step that does not exist (inremove,configure, orinsert.before/insert.after), assigns an unknown field viaconfigure, or omits bothafterandbeforeanchors on an inserted step. Misconfigurations now surface at start-up rather than producing inscrutable runtime failures. - Pipeline strategy dependency validation is fail-fast (Issue #33 §2.1) —
ExecutionStrategy.__init__andinsert_after/insert_beforenow raisePipelineDependencyError(new typed error, codePIPELINE_DEPENDENCY_ERROR) when a step'srequireskeys are not provided by any preceding step'sprovides. The error names the offending step and the missing keys. A newvalidate_dependencies: bool = Truekeyword onExecutionStrategy.__init__lets internal callers (e.g.Executor.stream's post-stream sub-strategy) opt out when assembling derived strategies from an already-validated parent. Both new errors are exported fromapcore. - Cross-language alignment (sync A-001) — Renamed
CircuitOpenError(codeCIRCUIT_OPEN) to canonicalCircuitBreakerOpenError(codeCIRCUIT_BREAKER_OPEN) to match TypeScript and Rust SDKs and the protocol spec. The legacyCircuitOpenErrorclass is retained as a deprecated subclass alias ofCircuitBreakerOpenErrorso existingexcept CircuitOpenError:blocks raising the legacy class continue to work; the legacy class will be removed in a future major release. The wire error code emitted byCircuitBreakerMiddlewareis nowCIRCUIT_BREAKER_OPENfor both classes. NewErrorCodes.CIRCUIT_BREAKER_OPENconstant added;ErrorCodes.CIRCUIT_OPENretained as a deprecated alias.CircuitBreakerOpenErroris exported from the top-levelapcorepackage. TaskStore.put→save(cross-language alignment D-10) — Renamed the canonical write method on theTaskStoreProtocol.InMemoryTaskStore.putis retained as a deprecated shim that delegates tosaveand emits aDeprecationWarning; it will be removed in a future minor release. InternalAsyncTaskManagercalls now route through a_savehelper that preferssaveand falls back toputfor legacy custom stores.TaskStatus.RETRYINGremoved (cross-language alignment D-12) — During retry backoff, the task status is nowTaskStatus.PENDINGto match the TypeScript and Rust SDKs and the protocol spec.TaskStatus.RETRYINGremains accessible for one minor release as a deprecated attribute that resolves toTaskStatus.PENDINGand emits aDeprecationWarningon access. The"retrying"enum value is no longer present inTaskStatus.__members__.TaskInfo.attempt_number→retry_count(cross-language alignment D-13) — Renamed the dataclass field.attempt_numberis retained as a deprecated property (with both getter and setter) that reads/writesretry_countand emits aDeprecationWarning. It will be removed in a future minor release.ErrorHistoryeviction is min-heap-based (PROTOCOL_SPEC Issue #43 §3) — Confirmed the in-place O(log N) min-heap eviction keyed onlast_occurredwith lazy deletion of stale entries from dedup-driven timestamp refreshes. Replaces the prior O(excess × M) linear scan for the global-oldest entry; per-insert eviction cost is bounded regardless of the number of tracked modules.AsyncTaskManager.start_reaperaligned with TS / Rust D-11 surface — acceptsttl_seconds(seconds) andsweep_interval_ms(milliseconds) keyword arguments and returns a newReaperHandle(withstop()/is_running()). The legacyinterval_seconds/max_age_secondsarguments still work but emitDeprecationWarning; passing both legacy and new aliases for the same value raisesTypeError.ReaperHandleis exported fromapcore.async_task.AsyncTaskManager.start_reaperdefaultsweep_interval_msaligned to 300_000 (sync alignment, WARNING #5) — Default sweep cadence changed from 3_600_000 ms (1 hour) to 300_000 ms (5 minutes), matching TypeScript and Rust. Callers that relied on the 1-hour default must now passsweep_interval_ms=3_600_000explicitly.
Fixed
- Async
on_errormiddleware now detects awaitable return values viainspect.isawaitable(...)rather thaninspect.iscoroutinefunction(mw.on_error)(#42). The previous gate missedfunctools.partialwrappers and decorator-wrapped async handlers (no__wrapped__), causing the recovery coroutine to be silently dropped —isinstance(recovery, dict)then evaluated against an un-awaited coroutine and the chain aborted. The same fix applies toexecute_beforeandexecute_after. Truly synchronous handlers continue to run throughasyncio.to_threadso blocking calls (time.sleepinRetryMiddleware) do not stall the event loop.
Added — PROTOCOL_SPEC hardening (Issues #32–#45)
- AsyncTaskManager Evolution (PROTOCOL_SPEC Issue #34) — Pluggable
TaskStoreprotocol withInMemoryTaskStoredefault; custom backends (Redis, SQL) can be injected at construction time. Per-task retry configuration via newRetryPolicydataclass (max_retries,retry_delay_ms,backoff_multiplier,max_retry_delay_ms) andBackoffStrategyenum; tasks move toTaskStatus.RETRYINGbetween attempts andFAILEDafter exhaustion.AsyncTaskManager.start_reaper(interval_seconds, max_age_seconds)/stop_reaper()— opt-in background task for automatic TTL-based deletion of terminal-state (COMPLETED,FAILED,CANCELLED) tasks. Exports:TaskStore,InMemoryTaskStore,RetryPolicy,BackoffStrategy. - Observability Hardening (PROTOCOL_SPEC Issue #43) — Pluggable
ObservabilityStoreprotocol withInMemoryObservabilityStoredefault (apcore.observability.store).BatchSpanProcessorfor non-blocking OTEL span export with configurable queue and drop-on-fullspans_droppedcounter (now exported fromapcore.observability.tracing). O(log N)ErrorHistoryeviction via min-heap keyed onlast_occurredplus O(1) fingerprint index replacing prior O(M) ring-buffer scan.compute_fingerprint()— SHA-256 content-addressable error deduplication with UUID/timestamp normalization (exported fromapcore.observability.error_history).RedactionConfiginContextLoggerfor globfield_patternsand regexvalue_patternsapplied at log time.PrometheusExporterHTTP server serving/metrics(Prometheus text format),/healthz(liveness), and/readyz(readiness) endpoints (apcore.observability.prometheus_exporter).MetricsCollector.export_prometheus()emitsapcore_module_calls_total,apcore_module_errors_total,apcore_module_duration_seconds.UsageCollector.export_prometheus()emitsapcore_usage_calls_total,apcore_usage_error_rate,apcore_usage_p50/p95/p99_latency_ms. - System Modules Hardening (PROTOCOL_SPEC Issue #45) —
overrides_pathparameter forregister_sys_modules()loads a YAML/JSON override file after base config on startup (viaAuditStore/OverridesStorepattern). Structured audit trail:AuditEntry,AuditStoreprotocol, andInMemoryAuditStoredefault record all state-modifying control-module calls with timestamp, action, actor_id, actor_type, trace_id, and before/after change dict (apcore.sys_modules.audit).fail_on_error: bool = Falseonregister_sys_modules()— whenTrueraisesSysModuleRegistrationError; whenFalse(default) logsERRORand continues.path_filterglob onsystem.control.reload_modulefor bulk reload in dependency topological order; mutually exclusive withmodule_id(raisesModuleReloadConflictErroron conflict). New error classes exported fromapcore:SysModuleRegistrationError(codeSYS_MODULE_REGISTRATION_FAILED),ModuleReloadConflictError(codeMODULE_RELOAD_CONFLICT). - Schema System Hardening (PROTOCOL_SPEC Issue #44) — New
apcore.schema.hardeningmodule:content_hash(schema)returns the SHA-256 of canonical JSON for content-addressable schema deduplication;validate_schema_dict(schema, data)usesDraft202012Validatorto exhaustively evaluate allanyOf/oneOfbranches (no short-circuit), resolve recursive$ref, enforce numerical/string constraints, and emit SHOULD-level warnings (not hard errors) on unrecognized format values. Conformance fixtures added:schema_hardening_union.json,schema_hardening_recursive.json,schema_hardening_constraints.json,schema_hardening_formats.json,schema_hardening_cache.json. - Multi-Class Module Discovery (PROTOCOL_SPEC §2.1.1) — New
apcore.registry.multi_classmodule:@multi_classdecorator opts a class into multi-class per-file scanning;class_name_to_segment()derives a snake_case ID segment from a class name (CamelCase →snake_case);discover_multi_class()scans a file and produces IDs of the formbase_id.class_segment. Single-class files with one decorated class receive the barebase_id(backward-compatible).ModuleIdConflictError(codeMODULE_ID_CONFLICT) raised when two classes in the same file produce identical snake_case segments. - Middleware Architecture Hardening (PROTOCOL_SPEC Issue #42) —
CircuitBreakerMiddlewaretracks per-module consecutive failures in a rolling window; transitions through CLOSED → OPEN → HALF_OPEN state machine. When OPEN,before()raisesCircuitOpenError(codeCIRCUIT_OPEN) to short-circuit execution entirely. On state changes emitsapcore.circuit.opened/apcore.circuit.closedevents.CircuitStateenum andCircuitBreakerMiddlewareare exported fromapcore. - Event Management Hardening (PROTOCOL_SPEC Issue #36) —
CircuitBreakerWrapperinapcore.events.circuit_breakerwraps anyEventSubscriberwith independent circuit-breaker protection (CLOSED/OPEN/HALF_OPEN state machine, configurableopen_threshold,recovery_window_ms); emitsapcore.subscriber.circuit_opened/apcore.subscriber.circuit_closedevents via the parentEventEmitter. - Conformance test suite expansion —
tests/conformance/test_pipeline_hardening.py(5 cases: fail-fast, continue-on-ignored-error, replace semantic,run_untiltermination, O(1) lookup),tests/conformance/test_schema_hardening.py(35 cases across union, recursive, constraints, formats, cache fixtures),tests/conformance/test_system_modules_hardening.py(10 cases: overrides persistence, audit entry extraction, Prometheus metrics, path_filter bulk reload, conflict error, fail_on_error behaviour).