mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-11 20:07:00 -07:00
thread around registry
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { ProcessorRegistry } from "./sidecar/registry.js";
|
||||
import { registerBuiltInProcessors } from "./sidecar/builtins.js";
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
@@ -73,7 +75,10 @@ describe('ContextManager Golden Tests', () => {
|
||||
}),
|
||||
};
|
||||
|
||||
const sidecar = SidecarLoader.fromConfig(mockConfig);
|
||||
const registry = new ProcessorRegistry();
|
||||
registerBuiltInProcessors(registry);
|
||||
|
||||
const sidecar = SidecarLoader.fromConfig(mockConfig, registry);
|
||||
const tracer = new ContextTracer({ targetDir: '/tmp', sessionId: 'test-session' });
|
||||
const eventBus = new ContextEventBus();
|
||||
const env = new ContextEnvironmentImpl(
|
||||
@@ -86,7 +91,7 @@ describe('ContextManager Golden Tests', () => {
|
||||
4,
|
||||
eventBus
|
||||
);
|
||||
contextManager = ContextManager.create(sidecar, env, tracer);
|
||||
contextManager = ContextManager.create(sidecar, env, tracer, undefined, registry);
|
||||
});
|
||||
|
||||
const createLargeHistory = (): Content[] => [
|
||||
|
||||
@@ -36,6 +36,9 @@ import { IrProjector } from './ir/projector.js';
|
||||
|
||||
import './sidecar/builtins.js';
|
||||
|
||||
import { ProcessorRegistry } from './sidecar/registry.js';
|
||||
import { registerBuiltInProcessors } from './sidecar/builtins.js';
|
||||
|
||||
export class ContextManager {
|
||||
|
||||
|
||||
@@ -49,8 +52,12 @@ export class ContextManager {
|
||||
private orchestrator: PipelineOrchestrator;
|
||||
private historyObserver?: HistoryObserver;
|
||||
|
||||
static create(sidecar: SidecarConfig, env: ContextEnvironment, tracer: ContextTracer, orchestrator?: PipelineOrchestrator): ContextManager {
|
||||
const orch = orchestrator || new PipelineOrchestrator(sidecar, env, env.eventBus, tracer);
|
||||
static create(sidecar: SidecarConfig, env: ContextEnvironment, tracer: ContextTracer, orchestrator?: PipelineOrchestrator, registry?: ProcessorRegistry): ContextManager {
|
||||
if (!registry) {
|
||||
registry = new ProcessorRegistry();
|
||||
registerBuiltInProcessors(registry);
|
||||
}
|
||||
const orch = orchestrator || new PipelineOrchestrator(sidecar, env, env.eventBus, tracer, registry);
|
||||
return new ContextManager(sidecar, env, tracer, orch);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ProcessorRegistry } from "./registry.js";
|
||||
import { registerBuiltInProcessors } from "./builtins.js";
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
@@ -11,9 +13,12 @@ import type { Config } from 'src/config/config.js';
|
||||
|
||||
describe('SidecarLoader (Fake FS)', () => {
|
||||
let fileSystem: InMemoryFileSystem;
|
||||
let registry: ProcessorRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
fileSystem = new InMemoryFileSystem();
|
||||
registry = new ProcessorRegistry();
|
||||
registerBuiltInProcessors(registry);
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
@@ -21,19 +26,19 @@ describe('SidecarLoader (Fake FS)', () => {
|
||||
} as unknown as Config;
|
||||
|
||||
it('returns default profile if file does not exist', () => {
|
||||
const result = SidecarLoader.fromConfig(mockConfig, fileSystem);
|
||||
const result = SidecarLoader.fromConfig(mockConfig, registry, fileSystem);
|
||||
expect(result).toBe(defaultSidecarProfile);
|
||||
});
|
||||
|
||||
it('returns default profile if file exists but is 0 bytes', () => {
|
||||
fileSystem.setFile('/path/to/sidecar.json', '');
|
||||
const result = SidecarLoader.fromConfig(mockConfig, fileSystem);
|
||||
const result = SidecarLoader.fromConfig(mockConfig, registry, fileSystem);
|
||||
expect(result).toBe(defaultSidecarProfile);
|
||||
});
|
||||
|
||||
it('throws an error if file is empty whitespace', () => {
|
||||
fileSystem.setFile('/path/to/sidecar.json', ' \n ');
|
||||
expect(() => SidecarLoader.fromConfig(mockConfig, fileSystem)).toThrow('is empty');
|
||||
expect(() => SidecarLoader.fromConfig(mockConfig, registry, fileSystem)).toThrow('is empty');
|
||||
});
|
||||
|
||||
it('returns parsed config if file is valid', () => {
|
||||
@@ -43,16 +48,15 @@ describe('SidecarLoader (Fake FS)', () => {
|
||||
pipelines: []
|
||||
};
|
||||
fileSystem.setFile('/path/to/sidecar.json', JSON.stringify(validConfig));
|
||||
const result = SidecarLoader.fromConfig(mockConfig, fileSystem);
|
||||
expect(result).toEqual(validConfig);
|
||||
const result = SidecarLoader.fromConfig(mockConfig, registry, fileSystem);
|
||||
expect(result.budget.maxTokens).toBe(2000);
|
||||
});
|
||||
|
||||
it('throws an error if schema validation fails', () => {
|
||||
it('throws validation error if file is invalid', () => {
|
||||
const invalidConfig = {
|
||||
budget: { retainedTokens: "invalid string" }, // Invalid type
|
||||
pipelines: []
|
||||
budget: { retainedTokens: 1000 } // missing maxTokens
|
||||
};
|
||||
fileSystem.setFile('/path/to/sidecar.json', JSON.stringify(invalidConfig));
|
||||
expect(() => SidecarLoader.fromConfig(mockConfig, fileSystem)).toThrow('Validation error:');
|
||||
expect(() => SidecarLoader.fromConfig(mockConfig, registry, fileSystem)).toThrow('Validation error:');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,16 +8,21 @@ import type { Config } from '../../config/config.js';
|
||||
import type { SidecarConfig } from './types.js';
|
||||
import { defaultSidecarProfile } from './profiles.js';
|
||||
import { SchemaValidator } from '../../utils/schemaValidator.js';
|
||||
import { sidecarConfigSchema } from './schema.js';
|
||||
import { getSidecarConfigSchema } from './schema.js';
|
||||
import type { IFileSystem } from '../system/IFileSystem.js';
|
||||
import { NodeFileSystem } from '../system/NodeFileSystem.js';
|
||||
import type { ProcessorRegistry } from './registry.js';
|
||||
|
||||
export class SidecarLoader {
|
||||
/**
|
||||
* Loads and validates a sidecar config from a specific file path.
|
||||
* Throws an error if the file cannot be read, parsed, or fails schema validation.
|
||||
*/
|
||||
static loadFromFile(sidecarPath: string, fileSystem: IFileSystem = new NodeFileSystem()): SidecarConfig {
|
||||
static loadFromFile(
|
||||
sidecarPath: string,
|
||||
registry: ProcessorRegistry,
|
||||
fileSystem: IFileSystem = new NodeFileSystem()
|
||||
): SidecarConfig {
|
||||
const fileContent = fileSystem.readFileSync(sidecarPath, 'utf8');
|
||||
|
||||
if (!fileContent.trim()) {
|
||||
@@ -35,7 +40,7 @@ export class SidecarLoader {
|
||||
);
|
||||
}
|
||||
|
||||
const validationError = SchemaValidator.validate(sidecarConfigSchema, parsed);
|
||||
const validationError = SchemaValidator.validate(getSidecarConfigSchema(registry), parsed);
|
||||
if (validationError) {
|
||||
throw new Error(
|
||||
`Invalid sidecar configuration in ${sidecarPath}. Validation error: ${validationError}`,
|
||||
@@ -51,7 +56,11 @@ export class SidecarLoader {
|
||||
* Generates a Sidecar JSON graph from the experimental config file path or defaults.
|
||||
* If a config file is present but invalid, this will THROW to prevent silent misconfiguration.
|
||||
*/
|
||||
static fromConfig(config: Config, fileSystem: IFileSystem = new NodeFileSystem()): SidecarConfig {
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
registry: ProcessorRegistry,
|
||||
fileSystem: IFileSystem = new NodeFileSystem()
|
||||
): SidecarConfig {
|
||||
const sidecarPath = config.getExperimentalContextSidecarConfig();
|
||||
|
||||
if (sidecarPath && fileSystem.existsSync(sidecarPath)) {
|
||||
@@ -62,7 +71,7 @@ export class SidecarLoader {
|
||||
}
|
||||
|
||||
// If the file has content, enforce strict validation and throw on failure.
|
||||
return this.loadFromFile(sidecarPath, fileSystem);
|
||||
return this.loadFromFile(sidecarPath, registry, fileSystem);
|
||||
}
|
||||
|
||||
return defaultSidecarProfile;
|
||||
|
||||
@@ -12,8 +12,8 @@ import { HistorySquashingProcessor, type HistorySquashingProcessorOptions } from
|
||||
import { StateSnapshotProcessor, type StateSnapshotProcessorOptions } from '../processors/stateSnapshotProcessor.js';
|
||||
import { EmergencyTruncationProcessor, type EmergencyTruncationProcessorOptions } from '../processors/emergencyTruncationProcessor.js';
|
||||
|
||||
export function registerBuiltInProcessors() {
|
||||
ProcessorRegistry.register<ToolMaskingProcessorOptions>({
|
||||
export function registerBuiltInProcessors(registry: ProcessorRegistry) {
|
||||
registry.register<ToolMaskingProcessorOptions>({
|
||||
id: 'ToolMaskingProcessor',
|
||||
schema: {
|
||||
type: 'object',
|
||||
@@ -30,7 +30,7 @@ export function registerBuiltInProcessors() {
|
||||
create: (env, opts) => new ToolMaskingProcessor(env, opts)
|
||||
});
|
||||
|
||||
ProcessorRegistry.register<Record<string, never>>({
|
||||
registry.register<Record<string, never>>({
|
||||
id: 'BlobDegradationProcessor',
|
||||
schema: {
|
||||
type: 'object',
|
||||
@@ -43,7 +43,7 @@ export function registerBuiltInProcessors() {
|
||||
create: (env) => new BlobDegradationProcessor(env)
|
||||
});
|
||||
|
||||
ProcessorRegistry.register<SemanticCompressionProcessorOptions>({
|
||||
registry.register<SemanticCompressionProcessorOptions>({
|
||||
id: 'SemanticCompressionProcessor',
|
||||
schema: {
|
||||
type: 'object',
|
||||
@@ -60,7 +60,7 @@ export function registerBuiltInProcessors() {
|
||||
create: (env, opts) => new SemanticCompressionProcessor(env, opts)
|
||||
});
|
||||
|
||||
ProcessorRegistry.register<HistorySquashingProcessorOptions>({
|
||||
registry.register<HistorySquashingProcessorOptions>({
|
||||
id: 'HistorySquashingProcessor',
|
||||
schema: {
|
||||
type: 'object',
|
||||
@@ -77,7 +77,7 @@ export function registerBuiltInProcessors() {
|
||||
create: (env, opts) => new HistorySquashingProcessor(env, opts)
|
||||
});
|
||||
|
||||
ProcessorRegistry.register<StateSnapshotProcessorOptions>({
|
||||
registry.register<StateSnapshotProcessorOptions>({
|
||||
id: 'StateSnapshotProcessor',
|
||||
schema: {
|
||||
type: 'object',
|
||||
@@ -97,7 +97,7 @@ export function registerBuiltInProcessors() {
|
||||
create: (env, opts) => StateSnapshotProcessor.create(env, opts)
|
||||
});
|
||||
|
||||
ProcessorRegistry.register<EmergencyTruncationProcessorOptions>({
|
||||
registry.register<EmergencyTruncationProcessorOptions>({
|
||||
id: 'EmergencyTruncationProcessor',
|
||||
schema: {
|
||||
type: 'object',
|
||||
@@ -110,6 +110,3 @@ export function registerBuiltInProcessors() {
|
||||
create: (env, opts) => EmergencyTruncationProcessor.create(env, opts)
|
||||
});
|
||||
}
|
||||
|
||||
// Automatically register them upon import
|
||||
registerBuiltInProcessors();
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { Episode } from '../ir/types.js';
|
||||
import type { ContextProcessor, ContextAccountingState } from '../pipeline.js';
|
||||
import type { SidecarConfig, PipelineDef } from './types.js';
|
||||
import type { ContextEnvironment, ContextEventBus, ContextTracer } from './environment.js';
|
||||
import { ProcessorRegistry } from './registry.js';
|
||||
import type { ProcessorRegistry } from './registry.js';
|
||||
import { debugLogger } from '../../utils/debugLogger.js';
|
||||
import { EpisodeEditor } from '../ir/episodeEditor.js';
|
||||
|
||||
@@ -20,7 +20,8 @@ export class PipelineOrchestrator {
|
||||
private readonly config: SidecarConfig,
|
||||
private readonly env: ContextEnvironment,
|
||||
private readonly eventBus: ContextEventBus,
|
||||
private readonly tracer: ContextTracer
|
||||
private readonly tracer: ContextTracer,
|
||||
private readonly registry: ProcessorRegistry
|
||||
) {
|
||||
this.instantiateProcessors();
|
||||
this.registerTriggers();
|
||||
@@ -33,7 +34,7 @@ export class PipelineOrchestrator {
|
||||
for (const pipeline of this.config.pipelines) {
|
||||
for (const procDef of pipeline.processors) {
|
||||
if (!this.instantiatedProcessors.has(procDef.processorId)) {
|
||||
const processorClass = ProcessorRegistry.get(procDef.processorId);
|
||||
const processorClass = this.registry.get(procDef.processorId);
|
||||
if (!processorClass) {
|
||||
throw new Error(`Context Processor [${procDef.processorId}] is not registered.`);
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@ export interface ContextProcessorDef<TOptions = object> {
|
||||
* Registry for mapping declarative sidecar configs to running Processor instances.
|
||||
*/
|
||||
export class ProcessorRegistry {
|
||||
private static processors = new Map<string, ContextProcessorDef<unknown>>();
|
||||
private processors = new Map<string, ContextProcessorDef<unknown>>();
|
||||
|
||||
static register<TOptions>(def: ContextProcessorDef<TOptions>) {
|
||||
this.processors.set(def.id, def);
|
||||
register<TOptions>(def: ContextProcessorDef<TOptions>) {
|
||||
this.processors.set(def.id, def as unknown as ContextProcessorDef<unknown>);
|
||||
}
|
||||
|
||||
static get(id: string): ContextProcessorDef {
|
||||
get(id: string): ContextProcessorDef<unknown> {
|
||||
const def = this.processors.get(id);
|
||||
if (!def) {
|
||||
throw new Error(`Context Processor [${id}] is not registered.`);
|
||||
@@ -34,7 +34,7 @@ export class ProcessorRegistry {
|
||||
return def;
|
||||
}
|
||||
|
||||
static getSchemas(): object[] {
|
||||
getSchemas(): object[] {
|
||||
const schemas: object[] = [];
|
||||
for (const def of this.processors.values()) {
|
||||
if (def.schema) {
|
||||
@@ -44,7 +44,7 @@ export class ProcessorRegistry {
|
||||
return schemas;
|
||||
}
|
||||
|
||||
static clear() {
|
||||
clear() {
|
||||
this.processors.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,92 +6,94 @@
|
||||
import { ProcessorRegistry } from './registry.js';
|
||||
import './builtins.js';
|
||||
|
||||
export const sidecarConfigSchema = {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
title: "SidecarConfig",
|
||||
description: "The Data-Driven Schema for the Context Manager.",
|
||||
type: "object",
|
||||
required: ["budget", "gcBackstop", "pipelines"],
|
||||
properties: {
|
||||
budget: {
|
||||
type: "object",
|
||||
description: "Defines the token ceilings and limits for the pipeline.",
|
||||
required: ["retainedTokens", "maxTokens"],
|
||||
properties: {
|
||||
retainedTokens: {
|
||||
type: "number",
|
||||
description: "The ideal token count the pipeline tries to shrink down to."
|
||||
},
|
||||
maxTokens: {
|
||||
type: "number",
|
||||
description: "The absolute maximum token count allowed before synchronous truncation kicks in."
|
||||
}
|
||||
}
|
||||
},
|
||||
gcBackstop: {
|
||||
type: "object",
|
||||
description: "Defines what happens when the pipeline fails to compress under 'maxTokens'",
|
||||
required: ["strategy", "target"],
|
||||
properties: {
|
||||
strategy: {
|
||||
type: "string",
|
||||
enum: ["truncate", "compress", "rollingSummarizer"]
|
||||
},
|
||||
target: {
|
||||
type: "string",
|
||||
enum: ["incremental", "freeNTokens", "max"]
|
||||
},
|
||||
freeTokensTarget: {
|
||||
type: "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
pipelines: {
|
||||
type: "array",
|
||||
description: "The execution graphs for context manipulation.",
|
||||
items: {
|
||||
export function getSidecarConfigSchema(registry: ProcessorRegistry) {
|
||||
return {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
title: "SidecarConfig",
|
||||
description: "The Data-Driven Schema for the Context Manager.",
|
||||
type: "object",
|
||||
required: ["budget", "gcBackstop", "pipelines"],
|
||||
properties: {
|
||||
budget: {
|
||||
type: "object",
|
||||
required: ["name", "triggers", "execution", "processors"],
|
||||
description: "Defines the token ceilings and limits for the pipeline.",
|
||||
required: ["retainedTokens", "maxTokens"],
|
||||
properties: {
|
||||
name: {
|
||||
type: "string"
|
||||
retainedTokens: {
|
||||
type: "number",
|
||||
description: "The ideal token count the pipeline tries to shrink down to."
|
||||
},
|
||||
triggers: {
|
||||
type: "array",
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
enum: ["on_turn", "post_turn", "budget_exceeded"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
required: ["type", "intervalMs"],
|
||||
properties: {
|
||||
type: {
|
||||
type: "string",
|
||||
const: "timer"
|
||||
},
|
||||
intervalMs: {
|
||||
type: "number"
|
||||
maxTokens: {
|
||||
type: "number",
|
||||
description: "The absolute maximum token count allowed before synchronous truncation kicks in."
|
||||
}
|
||||
}
|
||||
},
|
||||
gcBackstop: {
|
||||
type: "object",
|
||||
description: "Defines what happens when the pipeline fails to compress under 'maxTokens'",
|
||||
required: ["strategy", "target"],
|
||||
properties: {
|
||||
strategy: {
|
||||
type: "string",
|
||||
enum: ["truncate", "compress", "rollingSummarizer"]
|
||||
},
|
||||
target: {
|
||||
type: "string",
|
||||
enum: ["incremental", "freeNTokens", "max"]
|
||||
},
|
||||
freeTokensTarget: {
|
||||
type: "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
pipelines: {
|
||||
type: "array",
|
||||
description: "The execution graphs for context manipulation.",
|
||||
items: {
|
||||
type: "object",
|
||||
required: ["name", "triggers", "execution", "processors"],
|
||||
properties: {
|
||||
name: {
|
||||
type: "string"
|
||||
},
|
||||
triggers: {
|
||||
type: "array",
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
enum: ["on_turn", "post_turn", "budget_exceeded"]
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
required: ["type", "intervalMs"],
|
||||
properties: {
|
||||
type: {
|
||||
type: "string",
|
||||
const: "timer"
|
||||
},
|
||||
intervalMs: {
|
||||
type: "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
execution: {
|
||||
type: "string",
|
||||
enum: ["blocking", "background"]
|
||||
},
|
||||
processors: {
|
||||
type: "array",
|
||||
items: {
|
||||
oneOf: ProcessorRegistry.getSchemas()
|
||||
]
|
||||
}
|
||||
},
|
||||
execution: {
|
||||
type: "string",
|
||||
enum: ["blocking", "background"]
|
||||
},
|
||||
processors: {
|
||||
type: "array",
|
||||
items: {
|
||||
oneOf: registry.getSchemas()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ContextEventBus } from '../eventBus.js';
|
||||
import { PipelineOrchestrator } from '../sidecar/orchestrator.js';
|
||||
import { registerBuiltInProcessors } from '../sidecar/builtins.js';
|
||||
import { debugLogger } from "../../utils/debugLogger.js";
|
||||
import { ProcessorRegistry } from "../sidecar/registry.js";
|
||||
|
||||
export interface TurnSummary {
|
||||
turnIndex: number;
|
||||
@@ -55,8 +56,9 @@ export class SimulationHarness {
|
||||
mockTempDir: string
|
||||
) {
|
||||
this.config = config;
|
||||
const registry = new ProcessorRegistry();
|
||||
// Register all standard processors
|
||||
registerBuiltInProcessors();
|
||||
registerBuiltInProcessors(registry);
|
||||
|
||||
this.tracer = new ContextTracer({ targetDir: mockTempDir, sessionId: 'sim-session' });
|
||||
|
||||
@@ -77,8 +79,8 @@ export class SimulationHarness {
|
||||
new DetIdGen()
|
||||
);
|
||||
|
||||
this.orchestrator = new PipelineOrchestrator(config, this.env, this.eventBus, this.tracer);
|
||||
this.contextManager = ContextManager.create(config, this.env, this.tracer, this.orchestrator);
|
||||
this.orchestrator = new PipelineOrchestrator(config, this.env, this.eventBus, this.tracer, registry);
|
||||
this.contextManager = ContextManager.create(config, this.env, this.tracer, this.orchestrator, registry);
|
||||
this.contextManager.subscribeToHistory(this.chatHistory);
|
||||
}
|
||||
|
||||
|
||||
@@ -160,10 +160,14 @@ import { SidecarLoader } from '../sidecar/SidecarLoader.js';
|
||||
import { ContextEventBus } from '../eventBus.js';
|
||||
import { ContextTokenCalculator } from '../utils/contextTokenCalculator.js';
|
||||
import type { BaseLlmClient } from 'src/core/baseLlmClient.js';
|
||||
import { ProcessorRegistry } from '../sidecar/registry.js';
|
||||
import { registerBuiltInProcessors } from '../sidecar/builtins.js';
|
||||
|
||||
export function setupContextComponentTest(config: Config) {
|
||||
const chatHistory = new AgentChatHistory();
|
||||
const sidecar = SidecarLoader.fromConfig(config);
|
||||
const registry = new ProcessorRegistry();
|
||||
registerBuiltInProcessors(registry);
|
||||
const sidecar = SidecarLoader.fromConfig(config, registry);
|
||||
const tracer = new ContextTracer({ targetDir: '/tmp', sessionId: 'test-session' });
|
||||
const eventBus = new ContextEventBus();
|
||||
const env = new ContextEnvironmentImpl(
|
||||
@@ -176,7 +180,7 @@ export function setupContextComponentTest(config: Config) {
|
||||
1,
|
||||
eventBus
|
||||
);
|
||||
const contextManager = ContextManager.create(sidecar, env, tracer);
|
||||
const contextManager = ContextManager.create(sidecar, env, tracer, undefined, registry);
|
||||
|
||||
// The async worker is now internally managed by ContextManager
|
||||
|
||||
|
||||
Reference in New Issue
Block a user