Orchestrata Ultimate / Specs / 024-dependency-injection
024-dependency-injection: Dependency Injection Container
The DI container provides interface-based component registration, async resolution with constructor injection, scope management (singleton/transient/request), and circular dependency detection. Its scope is the plugin SDK's capability-gated resolve() touchpoint (plugin/context.py) and the WebSocket gateway's assembly root (gateway/server.py) — see the "Descoped (2026-07-06)" section below for why this is narrower than the original "universal composition backbone" framing.
At a glance
| Item | Value |
|---|---|
| Spec directory | specs/024-dependency-injection/ |
| Status line in the spec | Implemented (scope reduced — see "Descoped" section) |
| Requirement IDs defined | 53 |
With a @traces citation in src/ |
49 (92.5%) |
With a @traces citation in tests/ |
0 (0.0%) |
| Tracked files | 6 |
| RTM page | 024-dependency-injection |
Reading this spec
The Dependency Injection Container provides interface-based component registration, async resolution with constructor injection, scope management (singleton, transient and request) and circular dependency detection. Its scope was deliberately reduced: it now serves the plugin SDK's capability-gated resolve() touchpoint and the WebSocket gateway's assembly root, and a "Descoped" section in the spec explains why. Every other composition root wires its dependencies explicitly, with constructor arguments and setter calls, per the architecture standards, and an architecture checker enforces that contract.
The scope is seven user stories: component registration, component resolution and circular dependency detection (priority 0), container builder, instance registration and container registry (priority 1), and introspection (priority 2). The functional requirements cover interface registration, scope enforcement, constructor injection, lifecycle integration, cycle detection, factory support, instance registration and context propagation. The container hierarchy requirement is struck through as removed in the descoping. Requirement IDs use the REQ-024- prefix.
The design is a DIContainer that maps interfaces to implementations with scope metadata, a fluent ContainerBuilder, an async resolve() that walks the dependency graph, detects cycles and creates instances, a ContainerRegistry of named containers, thread-safe registration and lock-free resolution. The reasons given are loose coupling, swappable implementations, testability through the same interfaces, and automatic IComponent.initialize() on resolution.
In the code, nearly all citations are in orchestrata_ultimate/di/container (49 requirements), with one in orchestrata_ultimate/core/protocols/base. The per-requirement view is on the RTM page.
The spec's status line reads Implemented, with the scope reduced as described. It tracks 6 files and defines 53 requirement IDs. The full text follows below.
Measured coverage is 49 of 53 requirements cited by at least one @traces in src/ (92.5%), spread over 2 modules; the module citing the most of them is orchestrata_ultimate/di/container (49 requirements). In tests/, 0 requirements are cited by a test-side @traces.
Coverage gaps: 4 requirements have no citing symbol. The largest untraced groups are REQ-024-* (4). Untraced means no @traces names the ID; the behaviour may still exist in code that was never annotated. Test-side citations are thinner than code-side ones (0 against 49).
Requirements
Checkbox is the state recorded in the requirements checklist; Traced by is the count of src/ symbols whose @traces names the ID.
| ID | Done | Requirement | Traced by |
|---|---|---|---|
REQ-024-001 |
no | DIContainer.register() accepts an interface class and an implementation class and stores the mapping internally. | 1 |
REQ-024-002 |
no | register() accepts a scope parameter of type ComponentScope (SINGLETON, TRANSIENT, REQUEST) and defaults to SINGLETON when omitted. |
2 |
REQ-024-003 |
no | register() accepts an optional config dict that is stored with the registration and passed to IComponent.initialize() at resolution time. |
1 |
REQ-024-004 |
no | register() accepts an optional factory callable; when provided, the factory is used instead of instantiating the implementation class. |
1 |
REQ-024-005 |
no | When factory is None, register() validates that implementation is a subclass of interface and raises TypeError if it is not. |
1 |
REQ-024-006 |
no | register() raises ValueError when neither implementation nor factory is provided. |
1 |
REQ-024-007 |
no | The config dict is shallow-copied at registration time so subsequent mutation of the caller's dict does not affect the stored registration. |
1 |
REQ-024-008 |
no | register() accepts an optional dependencies list of types that explicitly override constructor parameter resolution order. |
1 |
REQ-024-009 |
no | register() returns self to allow method chaining. |
1 |
REQ-024-010 |
no | Registering the same interface a second time on the same container raises ValueError unless the container was constructed with allow_override=True. |
1 |
REQ-024-011 |
no | DIContainer.resolve() is an async method that returns an instance satisfying the requested interface. | 1 |
REQ-024-012 |
no | resolve() raises ValueError when the requested interface has no registration in the container. | 1 |
REQ-024-013 |
no | SINGLETON-scoped registrations return the same instance object on every call to resolve() for the same interface. | 1 |
REQ-024-014 |
no | TRANSIENT-scoped registrations return a new instance object on every call to resolve(). | 1 |
REQ-024-015 |
no | REQUEST-scoped registrations return the same instance within a single resolution tree and a new instance for each top-level resolve() call. | 1 |
REQ-024-016 |
no | After instantiating a class, resolve() calls await instance.initialize(config) if the instance is an IComponent. |
1 |
REQ-024-017 |
no | initialize() is called exactly once per singleton instance (not on every resolve). | 1 |
REQ-024-018 |
no | resolve() inspects the constructor (init) signature of the implementation class and resolves each type-annotated parameter recursively. | 1 |
REQ-024-019 |
no | Parameters without type annotations or with built-in types (str, int, etc.) are skipped during constructor injection. | 1 |
REQ-024-020 |
no | If the factory callable is registered, resolve() calls await factory(container) and skips constructor inspection. |
1 |
REQ-024-021 |
no | resolve() passes the stored config dict to the factory as a keyword argument when the factory signature accepts config. |
1 |
REQ-024-022 |
no | Resolved instances from register_instance() are returned directly without calling initialize() again. | none |
REQ-024-023 |
no | resolve() supports generic type parameters (e.g., resolve(IStore[int])) by falling back to the base generic alias if the parameterized form is unregistered. |
1 |
REQ-024-024 |
no | resolve() propagates exceptions raised inside factory callables or initialize() without wrapping them. | 1 |
REQ-024-025 |
no | The ResolutionContext cache is per-call (not shared across concurrent resolve calls) to prevent cross-request contamination. | 1 |
REQ-024-026 |
no | When resolving interface A causes a recursive resolve of A, a CircularDependencyError is raised before instantiation. | 1 |
REQ-024-027 |
no | The CircularDependencyError message contains the full dependency chain as a readable arrow-delimited string (e.g., IPostman -> IRouter -> IPostman). |
1 |
REQ-024-028 |
no | Circular detection works for transitive cycles of arbitrary depth (A→B→C→A). | 1 |
REQ-024-029 |
no | Diamond dependencies (A→B, A→C, B→D, C→D) do not trigger a false CircularDependencyError. | 1 |
REQ-024-030 |
no | The resolving stack is stored on ResolutionContext and is unwound correctly if an exception occurs mid-resolution. | 2 |
REQ-024-031 |
no | ContainerBuilder.with_component() accepts interface, implementation, scope, and config and returns self for chaining. |
1 |
REQ-024-032 |
no | ContainerBuilder.build() constructs and returns a fully configured DIContainer with all registered components. | 1 |
REQ-024-034 |
no | build() validates that all registrations are internally consistent (no duplicate interfaces, valid implementations) and raises ValueError on the first violation. | 1 |
REQ-024-035 |
no | CORRECTED 2026-07-06 (was a doc/code mismatch, D4 audit) — the DIContainer returned by build() is the SAME instance the builder has been mutating since construction, not an independent copy; subsequent with_* calls on the builder continue to mutate the container already handed to callers of build(). |
1 |
REQ-024-036 |
no | DIContainer.register_instance() accepts an interface type and a pre-created instance object. | 1 |
REQ-024-037 |
no | register_instance() raises ValueError if the instance does not implement (is not an instance of) the given interface. | 1 |
REQ-024-038 |
no | resolve() returns the pre-created instance directly for interfaces registered via register_instance(). | 1 |
REQ-024-039 |
no | Instances registered via register_instance() coexist with class-based registrations in the same container without conflict. | 1 |
REQ-024-040 |
no | register_instance() returns self to allow method chaining. |
none |
REQ-024-041 |
no | ContainerRegistry.register() stores a DIContainer under a given string name. | 1 |
REQ-024-042 |
no | ContainerRegistry.get() retrieves a previously registered container by name and raises KeyError if the name is unknown. | 1 |
REQ-024-043 |
no | ContainerRegistry.has() returns True if a container is registered under the given name, False otherwise. | 1 |
REQ-024-045 |
no | ContainerRegistry is a global singleton accessible without instantiation. | 1 |
REQ-024-046 |
no | DIContainer.get_registry_info() returns a dict mapping interface names to registration metadata (scope, implementation name, has_factory). | 1 |
REQ-024-047 |
no | DIContainer.has() returns True if the given interface has a registration, False otherwise. | 1 |
REQ-024-048 |
no | DIContainer.get_all_registrations() returns the raw mapping of interface type to ComponentRegistration dataclass. | 1 |
REQ-024-049 |
no | DIContainer.set_context() stores an arbitrary key-value pair accessible throughout resolution. | 1 |
REQ-024-050 |
no | DIContainer.get_context() retrieves a stored ambient value by key, returning the provided default if the key is absent. | 1 |
REQ-024-051 |
no | All write operations to the registration map (_registrations) and singleton cache (_singletons) are protected by a threading.Lock. | 1 |
REQ-024-052 |
no | Resolution (read path) does not hold the lock during async instantiation or initialize() calls to prevent deadlock under concurrent usage. | 1 |
REQ-024-053 |
no | Singleton creation uses a double-checked locking pattern: check without lock, acquire lock, check again, create if still absent. | 1 |
REQ-024-044 |
- | removed from | none |
REQ-024-033 |
- | ) — deleted because it bypassed register()'s own |
none |
Code that traces this spec
| Module | Symbols | Requirements |
|---|---|---|
orchestrata_ultimate/core/protocols/base |
1 | 1 |
orchestrata_ultimate/di/container |
23 | 49 |
Checklists
checklists/constitutional.md
Constitutional Checklist — SPEC-024: Dependency Injection Container
Security (CONST-024-001 to 005)
- [ ] CONST-024-001: The container does not execute arbitrary code from configuration dictionaries;
configvalues are data only and never eval'd or exec'd. - [ ] CONST-024-002: Factory callables must be registered explicitly by trusted bootstrap code; the container provides no mechanism to register factories from serialized or untrusted input.
- [ ] CONST-024-003: The container does not expose private attributes of registered instances through introspection APIs; get_registry_info() returns metadata only, never instance references for TRANSIENT components.
- [ ] CONST-024-004: ContainerRegistry access is not gated by authentication at the container level; callers are responsible for controlling access to the registry object itself.
- [ ] CONST-024-005: The container does not use Python reflection (getattr with dynamic attribute names from external input) beyond type-safe init inspection; all introspection is limited to type annotations.
Reliability (CONST-024-006 to 010)
- [ ] CONST-024-006: An exception raised during resolve() (including inside factory or initialize()) unwinds the resolving stack completely so subsequent calls are not affected by a partial resolution.
- [ ] CONST-024-007: The singleton cache is never left in a partially-initialized state; an instance is only cached after initialize() completes successfully.
- [ ] CONST-024-008: All public methods that mutate container state (register, register_instance, set_context) are safe to call from multiple threads concurrently without data corruption.
- [ ] CONST-024-009: The container does not swallow exceptions; all errors propagate to the caller with their original type and traceback intact.
- [ ] CONST-024-010: ContainerBuilder.build() is idempotent — calling it multiple times returns independent containers without cross-contamination of singleton caches.
Performance (CONST-024-011 to 013)
- [ ] CONST-024-011: Singleton resolution after the first call involves only a dict lookup and returns in O(1) time without acquiring the threading lock.
- [ ] CONST-024-012: Constructor injection parameter inspection is performed once per registration (cached) and not repeated on every resolve call.
- [ ] CONST-024-013: The container does not retain strong references to TRANSIENT instances after resolution completes, allowing the garbage collector to reclaim them promptly.
Interoperability (CONST-024-014 to 016)
- [ ] CONST-024-014: The container correctly handles IComponent subclasses that define initialize() as a coroutine (async def); synchronous initialize() methods are not supported and raise TypeError if encountered.
- [ ] CONST-024-015: The container integrates with Python's typing system — resolve() is generic (
async def resolve(self, interface: type[T]) -> T) so type checkers can infer the return type. - [ ] CONST-024-016: ComponentRegistration is a plain dataclass with no hidden state, making it serializable to JSON for logging and diagnostic purposes (scope stored as string value, factory stored as qualified name).
checklists/requirements.md
The requirement list above comes from this file; 0 of 51 checkbox items are ticked.
checklists/testing.md
Testing Checklist — SPEC-024: Dependency Injection Container
Each item maps 1:1 to a requirement in requirements.md.
Core Registration Tests
- [ ] TEST-024-001: Test that register() stores the interface→implementation mapping and the mapping is retrievable via get_all_registrations().
- [ ] TEST-024-002: Test that register() defaults scope to SINGLETON when no scope argument is passed and that the stored registration reflects this.
- [ ] TEST-024-003: Test that register() with a config dict stores the config and that the stored copy equals the original dict.
- [ ] TEST-024-004: Test that register() with a factory callable stores the factory and that resolve() calls it instead of instantiating the implementation.
- [ ] TEST-024-005: Test that register() with an implementation that does not subclass the interface raises TypeError with a descriptive message.
- [ ] TEST-024-006: Test that register() with neither implementation nor factory raises ValueError.
- [ ] TEST-024-007: Test that mutating the caller's config dict after register() does not affect the stored registration config.
- [ ] TEST-024-008: Test that register() with a
dependencieslist stores the list and that resolve() uses it to override constructor parameter order. - [ ] TEST-024-009: Test that register() returns the container instance (self) enabling method chaining in a single expression.
- [ ] TEST-024-010: Test that registering the same interface twice raises ValueError and that registering with allow_override=True succeeds and replaces the first registration.
Resolution Tests
- [ ] TEST-024-011: Test that resolve() is a coroutine and returns an instance of the registered implementation class.
- [ ] TEST-024-012: Test that resolve() raises ValueError when called with an interface that has no registration.
- [ ] TEST-024-013: Test that two awaited calls to resolve() for a SINGLETON-scoped registration return the identical object (is-check, not equality).
- [ ] TEST-024-014: Test that two awaited calls to resolve() for a TRANSIENT-scoped registration return different object instances.
- [ ] TEST-024-015: Test that REQUEST-scoped components return the same instance within one resolve tree and different instances across two separate top-level resolve calls.
- [ ] TEST-024-016: Test that resolve() calls initialize() on the returned instance when the instance is an IComponent subclass.
- [ ] TEST-024-017: Test that initialize() is called exactly once for a SINGLETON component even when resolve() is called three times.
- [ ] TEST-024-018: Test that resolve() correctly injects a typed constructor parameter by resolving its type from the container automatically.
- [ ] TEST-024-019: Test that parameters annotated with str or int are not resolved from the container and are left for default values or raise TypeError normally.
- [ ] TEST-024-020: Test that a registered factory callable is invoked and its return value is used as the resolved instance.
- [ ] TEST-024-021: Test that when the factory signature includes a
configkeyword argument, resolve() passes the stored config dict to it. - [ ] TEST-024-022: Test that an instance registered via register_instance() is returned by resolve() without any initialize() call being made.
- [ ] TEST-024-023: Test that resolve() for a parameterized generic (e.g., IStore[int]) falls back to the base class registration when no exact match exists.
- [ ] TEST-024-024: Test that an exception raised inside a factory propagates unchanged (same type and message) from resolve().
- [ ] TEST-024-025: Test that concurrent resolve() calls for the same TRANSIENT interface do not share a ResolutionContext or contaminate each other's REQUEST-scoped cache.
Circular Dependency Detection Tests
- [ ] TEST-024-026: Test that A resolving A (direct self-dependency) raises CircularDependencyError before any instantiation occurs.
- [ ] TEST-024-027: Test that the CircularDependencyError message contains all interface names in the cycle separated by " -> ".
- [ ] TEST-024-028: Test that a three-node cycle (A→B→C→A) raises CircularDependencyError with all three names in the message.
- [ ] TEST-024-029: Test that a diamond dependency graph (A→B, A→C, B→D, C→D) resolves successfully without raising CircularDependencyError.
- [ ] TEST-024-030: Test that after a CircularDependencyError, the container is still usable — a subsequent valid resolve() call succeeds.
Container Builder Tests
- [ ] TEST-024-031: Test that with_component() returns the builder instance for chaining and that all arguments are stored for later use by build().
- [ ] TEST-024-032: Test that build() returns a DIContainer instance with all registered components resolvable.
- [ ] TEST-024-033: Test that the builder accepts a list of ComponentRegistration objects and registers all of them in the built container.
- [ ] TEST-024-034: Test that build() raises ValueError when two with_component() calls register the same interface (without allow_override).
- [ ] TEST-024-035: Test that calling with_component() after build() does not modify the previously built container's registrations.
Instance Registration Tests
- [ ] TEST-024-036: Test that register_instance() accepts an interface and an object and the object is returned by resolve().
- [ ] TEST-024-037: Test that register_instance() raises ValueError when the instance is not an instance of the given interface.
- [ ] TEST-024-038: Test that resolve() for an instance-registered interface returns the exact object passed to register_instance() (identity check).
- [ ] TEST-024-039: Test that a container with both class-based and instance-based registrations resolves each correctly without interference.
- [ ] TEST-024-040: Test that register_instance() returns the container instance (self) enabling chaining.
Container Registry Tests
- [ ] TEST-024-041: Test that ContainerRegistry.register() stores a container under a given name and it is retrievable.
- [ ] TEST-024-042: Test that ContainerRegistry.get() raises KeyError for an unregistered name.
- [ ] TEST-024-043: Test that ContainerRegistry.has() returns True for a registered name and False for an unknown name.
- [ ] TEST-024-044: Test that a child container resolves from the parent when the interface is registered only on the parent.
- [ ] TEST-024-045: Test that ContainerRegistry is accessible as a module-level singleton without requiring instantiation.
Introspection Tests
- [ ] TEST-024-046: Test that get_registry_info() returns a dict containing keys for each registered interface with scope, implementation name, and has_factory fields.
- [ ] TEST-024-047: Test that has() returns True for a registered interface and False for an unregistered interface.
- [ ] TEST-024-048: Test that get_all_registrations() returns ComponentRegistration objects with all fields populated correctly.
- [ ] TEST-024-049: Test that set_context() stores a value and get_context() retrieves it by key within the same container.
- [ ] TEST-024-050: Test that get_context() returns the provided default value when the key has not been set.
Thread Safety Tests
- [ ] TEST-024-051: Test that concurrent register() calls from 50 threads on the same container produce a consistent registration map with no lost entries or corrupted state.
- [ ] TEST-024-052: Test that concurrent resolve() calls for a SINGLETON from 20 threads each return the identical instance and initialize() is called exactly once total.
- [ ] TEST-024-053: Test that under concurrent load, no two threads simultaneously create a singleton instance (verify via call counter that initialize() fires exactly once).
Other tracked files
specs/024-dependency-injection/README.mdspecs/024-dependency-injection/compliance-audit.md
Full spec text
Verbatim from specs/024-dependency-injection/spec.md; headings are pushed down two levels.
SPEC-024: Dependency Injection Container
Metadata
- Spec ID: 024-dependency-injection
- Title: Dependency Injection Container
- Author: Mohamed
- Status: Implemented (scope reduced — see "Descoped" section)
- Created: 2026-04-04
- Last Updated: 2026-07-06
- Priority: P0
- Version: 1.1.0
- Extends: 001-ultimate-architecture
- Components: DIContainer, ContainerBuilder, ComponentRegistration, ContainerRegistry
- Dependencies: orchestrata-core (IComponent, ComponentScope interfaces)
Overview
What
The DI container provides interface-based component registration, async resolution with constructor injection, scope management (singleton/transient/request), and circular dependency detection. Its scope is the plugin SDK's capability-gated resolve() touchpoint (plugin/context.py) and the WebSocket gateway's assembly root (gateway/server.py) — see the "Descoped (2026-07-06)" section below for why this is narrower than the original "universal composition backbone" framing.
Why
- The plugin SDK and the gateway need loose coupling — components depend on interfaces, not implementations
- Runtime flexibility: swap implementations (real Redis vs in-memory) without code changes
- Testability: register test doubles via the same interface
- Lifecycle management: IComponent.initialize() called automatically on resolution
How
- DIContainer registers interface→implementation mappings with scope metadata
- ContainerBuilder provides a fluent
with_component()/with_instance()/with_config()/with_context()API for assembling a container - Async resolve() walks the dependency graph, detects cycles, creates instances
- ContainerRegistry maintains named containers
- Thread-safe registration, lock-free resolution
- Every composition root outside plugin/gateway/di wires dependencies explicitly (constructor args, setter calls) per
architecture-standards.md§composition — this was already true for 8 of 9 real composition roots before this rewrite (D4 audit) and is now the documented, enforced (scripts/architecture_checker.py) contract rather than an unstated implementation detail.
User Scenarios and Stories
US-001: Component Registration (P0)
As a package author I want to register my component's interface and implementation So that other packages can resolve my component without knowing the concrete class
Acceptance Criteria:
- [ ] Register interface→implementation with register()
- [ ] Register with specific scope (singleton, transient, request)
- [ ] Register with configuration dictionary
- [ ] Register with factory callable instead of class
- [ ] Validate implementation is subclass of interface
- [ ] Reject registration without implementation or factory
US-002: Component Resolution (P0)
As a consuming package I want to resolve an interface to get an instance So that I can use the component without knowing its concrete type
Acceptance Criteria:
- [ ] Resolve interface returns configured instance
- [ ] Singleton scope returns same instance on repeated resolve
- [ ] Transient scope returns new instance each time
- [ ] IComponent.initialize() called with config on first resolution
- [ ] Raise ValueError for unregistered interface
- [ ] Constructor dependencies resolved recursively
US-003: Circular Dependency Detection (P0)
As a developer I want to get a clear error when circular dependencies exist So that I can fix the dependency graph
Acceptance Criteria:
- [ ] Detect A→B→A cycles during resolution
- [ ] Error message shows the full cycle path
- [ ] Detection works for transitive cycles (A→B→C→A)
- [ ] No false positives for diamond dependencies
US-004: Container Builder (P1)
As a system bootstrap I want to assemble a container from configuration So that the system can be configured declaratively
Acceptance Criteria:
- [ ] ContainerBuilder provides fluent with_component() API
- [ ] build() returns configured DIContainer
- [x]
Support batch registration from config dict— DESCOPED 2026-07-06 (with_components()deleted; see "Descoped" section) - [ ] Builder validates registrations before building
US-005: Instance Registration (P1)
As a test or bootstrap code I want to register pre-created instances So that I can inject specific objects (mocks, singletons)
Acceptance Criteria:
- [ ] register_instance() accepts interface and instance
- [ ] Instance returned on subsequent resolve calls
- [ ] Instance must implement the registered interface
- [ ] Works alongside class-based registrations
US-006: Container Registry (P1)
As the kernel I want to maintain named containers So that different subsystems can have isolated DI scopes
Acceptance Criteria:
- [ ] ContainerRegistry maintains named container map
- [ ] Register and retrieve containers by name
- [x]
Support container hierarchy (parent-child resolution)— DESCOPED 2026-07-06 (get_with_parent()deleted; see "Descoped" section)
US-007: Introspection (P2)
As a diagnostic tool I want to inspect container registrations So that I can debug wiring issues
Acceptance Criteria:
- [ ] get_registry_info() returns all registrations
- [ ] has() checks if interface is registered
- [ ] get_all_registrations() returns full registry
- [ ] set_context() / get_context() for ambient values
Terminology
| Term | Definition |
|---|---|
| Interface | Abstract base class (IComponent subclass) defining a contract |
| Implementation | Concrete class implementing an interface |
| Scope | Lifecycle policy: SINGLETON (one instance), TRANSIENT (new each time), REQUEST (per-request) |
| Resolution | Process of creating an instance by walking the dependency graph |
| Constructor Injection | Resolving dependencies by inspecting init parameter types |
| Factory | Callable that creates an instance, alternative to class-based registration |
| Circular Dependency | A→B→A cycle detected during resolution |
| Context | Key-value store for ambient values (e.g., config, session) |
Data Models
ComponentRegistration
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "ComponentRegistration",
"required": ["interface", "scope"],
"properties": {
"interface": {
"type": "string",
"description": "Fully qualified interface class name"
},
"implementation": {
"type": "string",
"description": "Fully qualified implementation class name"
},
"scope": {
"type": "string",
"enum": ["SINGLETON", "TRANSIENT", "REQUEST"],
"description": "Lifecycle scope of the component"
},
"config": {
"type": "object",
"description": "Configuration dictionary passed to initialize()"
},
"factory": {
"type": "object",
"description": "Callable factory function for instance creation"
},
"dependencies": {
"type": "array",
"items": {"type": "string"},
"description": "List of interface names this component depends on"
},
"is_instance": {
"type": "boolean",
"description": "Whether this is a pre-registered instance"
}
}
}
ResolutionContext
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "ResolutionContext",
"properties": {
"container": {
"type": "object",
"description": "The DIContainer performing resolution"
},
"resolution_stack": {
"type": "array",
"items": {"type": "string"},
"description": "Stack of interfaces being resolved (for cycle detection)"
},
"singletons": {
"type": "object",
"description": "Map of singleton instances keyed by interface name"
},
"context": {
"type": "object",
"description": "Ambient key-value store for resolution"
},
"request_id": {
"type": "string",
"description": "Unique identifier for this resolution request"
}
}
}
ComponentScope
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "ComponentScope",
"enum": ["SINGLETON", "TRANSIENT", "REQUEST"],
"description": "Defines the lifecycle of a component within the container"
}
Functional Requirements
FR-001: Interface Registration
The container MUST allow registration of interface-to-implementation mappings with configurable scope and optional configuration dictionary.
FR-002: Scope Enforcement
The container MUST enforce scope policies: SINGLETON returns same instance, TRANSIENT creates new instance, REQUEST creates instance per request context.
FR-003: Constructor Injection
The container MUST automatically resolve constructor dependencies by inspecting init parameter type annotations.
FR-004: Lifecycle Integration
The container MUST call IComponent.initialize(config) on first resolution of each component.
FR-005: Circular Dependency Detection
The container MUST detect circular dependencies during resolution and raise an error with the complete cycle path.
FR-006: Factory Support
The container MUST support factory callables as an alternative to class-based registration.
FR-007: Instance Registration
The container MUST support registering pre-created instances for testing and singleton scenarios.
FR-008: Container Hierarchy — REMOVED 2026-07-06
The container MUST support parent-child hierarchy for scoped resolution across container boundaries. Descoped under decision D4 — see the "Descoped (2026-07-06)" section below for the full rationale. The FR number is kept as a tombstone (not renumbered) so historical references stay stable.
FR-009: Context Propagation
The container MUST maintain ambient context values accessible during resolution.
FR-010: Thread Safety
The container MUST be thread-safe for concurrent registration and resolution operations.
FR-011: Fluent Builder API
The container builder MUST provide a fluent API for declarative container assembly.
FR-012: Named Container Registry
The system MUST maintain a registry of named containers for multi-tenant or multi-environment scenarios.
FR-013: Introspection Capabilities
The container MUST provide introspection methods to query registered components and their metadata.
Error Conditions
| ID | Condition | Handling |
|---|---|---|
| ERR-001 | Resolve unregistered interface | Raise ValueError with message "No registration found for {interface.name}" |
| ERR-002 | Circular dependency detected | Raise CircularDependencyError with path showing full cycle |
| ERR-003 | Implementation not subclass of interface | Raise TypeError with message "{impl.name} does not implement {interface.name}" |
| ERR-004 | Instance type does not implement interface | Raise ValueError with message "Instance of {type(instance).name} does not implement {interface.name}" |
| ERR-005 | Registration without implementation or factory | Raise ValueError with message "Either implementation or factory must be provided" |
| ERR-006 | Duplicate interface registration without allow_override | Raise ValueError with message "Interface {interface.name} already registered" |
| ERR-007 | Invalid scope value | Raise ValueError with message "Invalid scope: {scope}. Must be SINGLETON, TRANSIENT, or REQUEST" |
| ERR-008 | Factory returns wrong type | Raise TypeError with message "Factory for {interface.name} returned {type(result).name}, expected {interface.name}" |
| ERR-009 | REMOVED 2026-07-06 — hierarchical resolve (FR-008) was descoped under D4; never implemented (no code path could raise this). See "Descoped" section. | |
| ERR-010 | Context key collision during resolution | Raise ValueError with message "Context key '{key}' already exists" |
| ERR-011 | Request scope without request context | Raise RuntimeError with message "REQUEST scoped component requires active request context" |
| ERR-012 | Async factory in synchronous resolve | Raise TypeError with message "Async factory cannot be called in synchronous context" |
| ERR-013 | Circular dependency in registration | Raise ValueError with message "Circular dependency in registration: {interfaces}" |
Descoped (2026-07-06)
Ruled under decision D4 (plans/decisions-2026-07-06.md; evidence:
plans/evidence/d4-di-container.md). An adoption audit of every real
composition root in the codebase found that 8 of the 9 (kernel/cli_main.py,
kernel/cli.py, postman/cli.py, agents/runtime_main.py,
knowledge_graph/server.py, the root cli.py, session/cli/main.py,
harness/cli.py) never touch DIContainer at all — they hand-construct
their components directly. The 9th (gateway/server.py) constructs exactly
one DIContainer, registers exactly one instance through it, and wires
every other dependency via 9 direct setter calls into gateway/rpc.py. This
container was never the "universal composition backbone" the original spec
described; that framing was aspirational, not descriptive.
The clearest evidence: this spec's flagship differentiator, container
hierarchy (FR-008), was an unimplemented stub —
ContainerRegistry.get_with_parent()'s entire body was container = cls.get (name) plus a comment ("Parent-child delegation would be implemented
here") and a return container. No delegation logic existed. Its only
callers were three unit tests exercising the stub itself; there were zero
real (non-test) consumers. docs/rtm.json nonetheless marked its
requirement "VERIFIED" — that RTM semantic means "a @traces decorator is
present," not "the behavior was functionally tested," and this is exactly
the false-confidence gap that prompted the D4 audit.
Rather than build out a never-used hierarchy feature to satisfy the letter
of the original FR-008, this rewrite takes the honest path: descope it, and
reframe the container's documented role to match what it actually,
load-bearingly does — the plugin SDK's capability-gated resolve()
touchpoint (plugin/context.py) and the WebSocket gateway's single
composition root (gateway/server.py). Every other composition root
continues to wire dependencies explicitly (constructor arguments, setter
calls) per architecture-standards.md §composition — this was already true
for 8 of 9 roots before this rewrite; it is now the documented, enforced
contract rather than an unstated implementation detail.
scripts/architecture_checker.py gained a standing rule (D4) that flags any
new DIContainer(...) construction outside plugin/, gateway/, or
di/ (test files are exempt — tests constructing components directly by
design is the norm in this suite, not a violation).
Removed:
- FR-008 (Container Hierarchy) and ERR-009 (parent-container-not-found
error) —
ContainerRegistry.get_with_parent()deleted fromsrc/orchestrata_ultimate/di/container.py. REQ-024-044 removed fromchecklists/requirements.md(no other code references it). - Batch registration via
ContainerBuilder.with_components()(the US-004 acceptance criterion "Support batch registration from config dict", backing REQ-024-033) — deleted because it bypassedregister()'s own duplicate-registration and subclass validation by writing directly into the private_registrydict (self._container._registry[...] = ...); its only real callers were unit tests. Batch registration is still achievable by callingwith_component()once per component — each call goes throughregister()'s validation like any other registration. REQ-024-033 removed fromchecklists/requirements.md.
Not affected: FR-012 (Named Container Registry) —
ContainerRegistry.register() / .get() / .has() / .shutdown_all() are
unrelated to hierarchy and remain fully implemented; only the parent-
delegation stub (get_with_parent()) was removed. US-006's "maintain named
container map" and "register/retrieve by name" acceptance criteria are
unaffected — only its "container hierarchy" bullet is descoped.
Also fixed during this pass (not a scope change, defect corrections):
ContainerBuilder.build()'s docstring claimed it "returns independent container"; it has always returned the sameDIContainerinstance the builder mutates (return self._container). The docstring now says so.DIContainer.shutdown()iterated onlyself._context, so a SINGLETON resolved through the normalresolve()path (which caches inself._singletons, a separate dict) never hadstop()called unless a caller also manually stashed the instance intoself._context. Fixed:shutdown()now stops both — context-tracked components (unchanged behavior) and_singletonsin reverse resolution order — without double-stopping an instance present in both.
API Contracts
DIContainer
| Method | Signature | Description |
|---|---|---|
| register | (interface, implementation, scope, config, dependencies, factory) -> self |
Register component |
| register_instance | (interface, instance) -> self |
Register pre-created instance |
| resolve | async (interface) -> T |
Resolve interface to instance |
| has | (interface) -> bool |
Check if interface is registered |
| get_registry_info | () -> dict |
Return all registration metadata |
| get_all_registrations | () -> dict[type, ComponentRegistration] |
Return raw registration map |
| set_context | (key, value) -> None |
Set ambient context value |
| get_context | (key, default) -> Any |
Get ambient context value |
ContainerBuilder
| Method | Signature | Description |
|---|---|---|
| with_component | (interface, implementation, scope, config) -> self |
Add registration |
| build | () -> DIContainer |
Build and return container |
ContainerRegistry
| Method | Signature | Description |
|---|---|---|
| register | (name, container) -> None |
Register named container |
| get | (name) -> DIContainer |
Retrieve named container |
| has | (name) -> bool |
Check if named container exists |
Non-Functional Requirements
Performance
- Resolution time complexity: O(n) where n is depth of dependency graph
- Registration: O(1) amortized
- Singleton resolution after first call: O(1)
Reliability
- Thread-safe registration and resolution
- No deadlocks on nested resolution calls
- Deterministic resolution order for same configuration
Compatibility
- Python 3.11+ required
- Backward compatible within major version
Technical Design
Registration Flow
- Client calls
register(interface, implementation, scope, config, factory) - Validate implementation is subclass of interface (or factory provided)
- Validate scope is valid enum value
- Create ComponentRegistration with provided metadata
- Store in internal registry map under interface key
- If allow_override=False and interface exists, raise ERR-006
Resolution Flow
- Client calls
await resolve(interface) - Check if interface registered, raise ERR-001 if not
- Check resolution_stack for cycle, raise ERR-002 if detected
- Add interface to resolution_stack
- If singleton and instance exists in singletons cache, return it
- Get ComponentRegistration for interface
- If factory provided, call factory; else instantiate implementation
- For each init parameter, recursively resolve dependency
- Call component.initialize(config) if IComponent
- If singleton, store instance in singletons cache
- Remove interface from resolution_stack
- Return instance
Thread Safety Design
- Registration uses
threading.RLockfor mutual exclusion - Resolution does not hold lock to prevent deadlock on nested resolve
- Singleton check-then-create uses double-checked locking pattern
Examples
Basic Registration and Resolution
from orchestrata.di import DIContainer, ComponentScope
container = DIContainer()
class IService(Interface):
pass
class ServiceImpl(IService):
async def initialize(self, config):
print("Initialized")
container.register(IService, ServiceImpl, ComponentScope.SINGLETON, {"timeout": 30})
service = await container.resolve(IService)
Using ContainerBuilder
from orchestrata.di import ContainerBuilder, ComponentScope
config = {
"db": {"interface": "IDatabase", "impl": "PostgresDatabase", "scope": "SINGLETON"},
"cache": {"interface": "ICache", "impl": "RedisCache", "scope": "TRANSIENT"}
}
builder = ContainerBuilder()
for name, cfg in config.items():
builder.with_component(
interface=cfg["interface"],
implementation=cfg["impl"],
scope=ComponentScope(cfg["scope"])
)
container = builder.build()
Testing with Instance Registration
class MockService(IService):
pass
mock = MockService()
container.register_instance(IService, mock)
resolved = await container.resolve(IService)
assert resolved is mock # Returns the same mock instance
Security
Input Validation
- All interface and implementation parameters MUST be validated as class types
- Factory callables MUST be validated to be callable
- Configuration dictionaries SHOULD be validated against schema if provided
Audit Logging
- Registration operations MUST log the interface name and scope
- Resolution operations MAY log for debugging (avoid logging sensitive config values)
Access Control
- Container access should be restricted to trusted code
- Context values containing secrets should be handled according to SPEC-099
Governance
Deprecation Policy
- Deprecated components will be marked with deprecation warning
- Major version may remove deprecated features
- Migration path documented in release notes
Breaking Changes
- Changes to registration API require major version bump
- Scope behavior changes require major version bump
- (Container hierarchy was never shipped as a working feature — descoped 2026-07-06, see "Descoped" section — so no version-bump policy applies to it.)
Visual Architecture
Class Diagram
Diagram source shown as text: Mermaid could not parse it (Error: Parse error on line 239:).
classDiagram
class MultiAgentEditResult {
+file_path: str
+old_string: str
+new_string: str
+agent_results: dict[Any]
+aggregated_result: dict[Any]
+timestamp: datetime = Field()
+success: bool = True
}
class GovernanceError {
}
class OrchestrataKernel {
+__init__(self): None
+_initialize_agents(self): None
+get_agents_by_type(self, agent_type): list[Any]
+get_agent_by_id(self, agent_id): Any
+async broadcast_to_type(self, agent_type, message): list[Any]
+async broadcast_to_all(self, message): list[Any]
+async send_to_agent(self, agent_id, message, sender): Any
}
class UltimateOrchestrator {
+__init__(self): None
+async execute_multi_agent_edit(self, file_path, old_string, new_string): MultiAgentEditResult
+_aggregate_agent_results(self, results): dict[Any]
+async create_project(self, name, path, template): dict[Any]
+async shutdown(self): None
}
class ThreadPoolTaskStatus {
+PENDING = 'pending'
+RUNNING = 'running'
+COMPLETED = 'completed'
+FAILED = 'failed'
+CANCELLED = 'cancelled'
+TIMEOUT = 'timeout'
}
class ThreadPoolTask {
+task_id: str = field()
+function: Any = None
+args: tuple = field()
+kwargs: dict[Any] = field()
+status: ThreadPoolTaskStatus = .PENDING
+created_at: datetime = field()
+started_at: Any = None
+completed_at: Any = None
+result: Any = None
+error: Any = None
+traceback: Any = None
+future: Any = None
+metadata: dict[Any] = field()
}
class ThreadPoolManager {
+__init__(self, max_workers, thread_name_prefix, initializer, initargs)
+start(self): None
+stop(self, wait, cancel_futures): None
+submit(self, fn, *args, **kwargs): str
+submit_many(self, functions, args_list, kwargs_list): list[str]
+async submit_async(self, fn, *args, **kwargs): T
+async submit_many_async(self, functions, args_list, kwargs_list): list[T]
+async wait_for_task(self, task_id, timeout): Any
+get_task_status(self, task_id): ThreadPoolTaskStatus
+get_task_result(self, task_id, wait): Any
+cancel_task(self, task_id): bool
+get_stats(self): dict[Any]
+_wrap_function(self, task_id, fn, *args, **kwargs): T
+_update_task_from_future(self, task_id, future): None
+task_context(self, task_id): Any
+async async_task_context(self, task_id): Any
}
class ParallelTestExecutor {
+__init__(self, thread_pool, max_workers)
+run_tests_parallel(self, test_paths, pytest_args, timeout_per_test): dict[Any]
+_run_single_test(self, test_path, pytest_args, timeout): dict[Any]
+async run_tests_parallel_async(self, test_paths, pytest_args, timeout_per_test): dict[Any]
}
class LogLevel {
+TRACE = 'TRACE'
+DEBUG = 'DEBUG'
+INFO = 'INFO'
+WARNING = 'WARNING'
+ERROR = 'ERROR'
+CRITICAL = 'CRITICAL'
}
class LogContext {
+__init__(self, logger, **context)
+__enter__(self)
+__exit__(self, exc_type, exc_val, exc_tb)
}
class LoggingContext {
+request_id: Any = None
+session_id: Any = None
+trace_id: Any = None
+span_id: Any = None
+user_id: Any = None
+user_email: Any = None
+user_role: Any = None
+component: Any = None
+operation: Any = None
+extra: dict[Any] = field()
+created_at: datetime = field()
---
+to_dict(self): dict[Any]
}
class ContextManager {
+get_current_context(): LoggingContext
+set_current_context(context): None
+clear_context(): None
+update_context(**kwargs): LoggingContext
+create_child_context(**updates): LoggingContext
}
class AsyncContext {
+__init__(self, **context_updates)
+async __aenter__(self): LoggingContext
+async __aexit__(self, exc_type, exc_val, exc_tb)
}
class Context {
+__init__(self, **context_updates)
+__enter__(self): LoggingContext
+__exit__(self, exc_type, exc_val, exc_tb)
}
class AuditAction {
+LOGIN = 'login'
+LOGOUT = 'logout'
+AUTH_FAILURE = 'auth_failure'
+PERMISSION_CHECK = 'permission_check'
+PERMISSION_DENIED = 'permission_denied'
+READ = 'read'
+CREATE = 'create'
+UPDATE = 'update'
+DELETE = 'delete'
+EXPORT = 'export'
+IMPORT = 'import'
+CONFIG_CHANGE = 'config_change'
+SERVICE_START = 'service_start'
+SERVICE_STOP = 'service_stop'
+BACKUP = 'backup'
+RESTORE = 'restore'
+USER_CREATE = 'user_create'
+USER_UPDATE = 'user_update'
+USER_DELETE = 'user_delete'
+ROLE_ASSIGN = 'role_assign'
+ROLE_REVOKE = 'role_revoke'
+EXTERNAL_CALL = 'external_call'
+WEBHOOK_RECEIVED = 'webhook_received'
+API_CALL = 'api_call'
+WORKFLOW_START = 'workflow_start'
+WORKFLOW_COMPLETE = 'workflow_complete'
+ESCALATION = 'escalation'
+APPROVAL = 'approval'
+REJECTION = 'rejection'
}
class AuditStatus {
+SUCCESS = 'success'
+FAILURE = 'failure'
+PARTIAL = 'partial'
+PENDING = 'pending'
}
class AuditEvent {
+action: AuditAction
+timestamp: datetime = field()
+status: AuditStatus = .SUCCESS
+actor_id: Any = None
+actor_type: str = 'user'
+actor_ip: Any = None
+actor_user_agent: Any = None
+resource_type: Any = None
+resource_id: Any = None
+resource_path: Any = None
+operation_id: Any = None
+operation_name: Any = None
+operation_duration_ms: Any = None
+before_state: Any = None
+after_state: Any = None
+changes: Any = None
+request_id: Any = None
+session_id: Any = None
+trace_id: Any = None
+span_id: Any = None
+component: Any = None
+severity: str = 'info'
+details: Any = None
+error_message: Any = None
+error_stack: Any = None
+event_hash: Any = None
+previous_hash: Any = None
---
+__post_init__(self)
+calculate_hash(self): str
+_json_serializer(self, obj)
+to_dict(self): dict[Any]
+verify_hash(self): bool
}
class AuditLogger {
+__init__(self, component)
+log_event(self, event): str
+log_action(self, action, status, resource_type, resource_id, before_state, after_state, actor_id, actor_type, details, error_message, operation_duration_ms, severity): str
}
class AuditOperation {
+action: Any
+resource_type: Any = None
+resource_id: Any = None
+before_state: Any = None
+actor_id: Any = None
+actor_type: str = 'user'
+component: Any = None
+details: Any = None
---
+__enter__(self)
+__exit__(self, exc_type, exc_val, exc_tb)
}
class DomainEntity {
+entity_id: str = Field()
+is_active: bool = Field()
---
+__init__(self, **data)
+mark_inactive(self)
+to_domain_dict(self): dict[Any]
+get_business_key(self): str
+__eq__(self, other): bool
+__hash__(self): int
+__repr__(self): str
}
class Node {
+node_id: str = Field()
+node_type: str = Field()
+name: str = Field()
+properties: dict[Any] = Field()
+created_at: datetime = Field()
+updated_at: datetime = Field()
+version: int = Field()
---
+to_graph_dict(self): dict[Any]
+from_graph_dict(cls, data): Node
+update_properties(self, **kwargs)
+__str__(self): str
}
class Config {
+arbitrary_types_allowed = True
+json_encoders = {}
}
class TracingLevel {
+OFF = 0
+BASIC = 1
+DETAILED = 2
+DEBUG = 3
---
+label(self): str
}
class TracingConfig {
+model_config = ConfigDict()
+enabled: bool = Field()
+service_name: str = Field()
+service_version: str = Field()
+endpoint: Any = Field()
+sample_rate: float = Field()
+console_export: bool = Field()
+tracing_level: TracingLevel = Field()
---
+clamp_sample_rate(cls, v): float
}
class MetricsConfig {
+__init__(self, enabled, port, host, include_system_metrics, system_metrics_interval)
}
class RetryStrategy {
+__init__(self, max_retries, initial_delay, max_delay, backoff_factor, jitter)
+calculate_delay(self, attempt): float
}
class CircuitState {
+CLOSED = 'closed'
+OPEN = 'open'
+HALF_OPEN = 'half_open'
}
class CircuitBreaker {
+__init__(self, failure_threshold, reset_timeout, half_open_max_requests, name)
+is_closed(self): bool
+is_open(self): bool
+is_half_open(self): bool
+record_success(self): None
+record_failure(self): None
+should_allow_request(self): bool
+async execute(self, operation): T
}
class Bulkhead {
+__init__(self, max_concurrent, max_queue_size, name)
+async execute(self, operation): T
}
Node <|-- DomainEntity
Dependencies
| Dependency | Version | Purpose |
|---|---|---|
| orchestrata-core | >=1.0.0 | IComponent, ComponentScope interfaces |
| threading | stdlib | Thread safety for container operations |
| contextvars | stdlib | Request scope context management |
Testing
Unit Tests
- Test each scope behavior (singleton, transient, request)
- Test circular dependency detection with various graph structures
- Test factory registration and invocation
- Test instance registration
- Test thread safety under concurrent access
Integration Tests
- Test container builder with complex configuration
- (Container hierarchy resolution — descoped 2026-07-06, see "Descoped" section; no longer a test target)
- Test IComponent.initialize() lifecycle integration
- Test context propagation through resolution chain
- Test shutdown() stops SINGLETON-scope components (D4 defect fix — previously only context-tracked components were stopped)
Coverage Targets
- Minimum 90% line coverage for DIContainer
- Minimum 85% line coverage for ContainerBuilder
- All error conditions must have test coverage
Integration Points
With orchestrata-core
- Uses IComponent interface for lifecycle management
- Uses ComponentScope enum for scope definitions
- Integrates with ComponentRegistry for discovery
With Package System
- Packages register components during bootstrap
- Configuration loaded from package manifests
- Resolution supports cross-package dependencies
With Request Pipeline
- REQUEST scoped components use request context
- Context values propagated through resolution
- Cleanup after request completion
Success Criteria
Must Have
- [x] Interface-based registration with scope support
- [x] Constructor injection with automatic dependency resolution
- [x] Singleton, Transient, and Request scope implementations
- [x] Circular dependency detection with clear error messages
- [x] Thread-safe registration and resolution
- [x] Container builder for declarative assembly
Should Have
- [x] Container registry for named containers
- [x] Introspection capabilities (has, get_registry_info)
- [x] Context propagation
Metrics
- Resolution latency: <1ms for cached singleton
- Registration latency: <0.1ms
- Memory overhead: O(n) where n is registered component count
Observability
Logging Requirements
- Component Logging
- Log all major operations with appropriate log levels
- Include correlation IDs for request tracing
- Log performance metrics for critical operations
- Log errors with full context for debugging
Metrics Requirements
- Performance Metrics
- Operation duration histograms
- Request/error rate counters
- Resource utilization gauges
- Business metrics relevant to domain
Tracing Requirements
- Distributed Tracing
- Trace propagation across component boundaries
- Span attributes for domain-specific context
- Trace sampling based on operational needs
Alerting Requirements
- Critical Alerts
- Error rate spikes
- Performance degradation
- Resource exhaustion
- Business metric anomalies
Dashboard Requirements
- Operational Dashboard
- Real-time system health
- Performance trends
- Error analysis
- Resource monitoring
Revision History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0.0 | 2026-04-04 | Mohamed | Initial specification |
| 1.0.1 | 2026-04-09 | Mohamed | Added missing sections per SPEC_STANDARD, expanded error conditions to 13, added functional requirements FR-001 to FR-013, added JSON schemas for data models |
| 1.1.0 | 2026-07-06 | Mohamed | D4: descoped FR-008 (container hierarchy, get_with_parent()) and ERR-009 — unimplemented stub, zero real consumers, falsely RTM-VERIFIED; descoped with_components() batch registration (REQ-024-033) — bypassed register() validation via private _registry writes. Reframed the container's documented role as plugin SDK + gateway assembly (8 of 9 composition roots already wired explicitly). Fixed build()'s "independent container" doc/code mismatch and a shutdown() defect (SINGLETON components resolved via resolve() were never stopped). See "Descoped (2026-07-06)" section. |