From 4d8d4b354ba9aa210f89aae2cad257ac64f08bf1 Mon Sep 17 00:00:00 2001 From: lr Date: Wed, 27 May 2026 10:34:34 +0800 Subject: [PATCH] feat: add lane config option for embedded agent isolation Add optional `lane` parameter to CleanContextRunnerOptions that allows extraction tasks (L1/L2/L3) to use a dedicated global lane instead of the default "main" lane. This prevents long-running extraction calls from blocking interactive chat sessions. Changes: - CleanContextRunnerOptions: new `lane` field, passed to runEmbeddedPiAgent() as `params.lane` - SceneExtractorOptions: accept `lane`, forward to CleanContextRunner - PersonaGenerator: accept `lane`, forward to CleanContextRunner - L1 extractor (callLlmExtraction): accept `lane`, forward to runner - L1 dedup (runLlmJudgment): accept `lane`, forward to runner - README: document extraction.lane and persona.lane Usage: "memory-tencentdb": { "extraction": { "model": "deepseek/deepseek-v4-flash", "lane": "cron" }, "persona": { "lane": "cron" } } Setting lane="cron" routes extraction to the "cron-nested" global lane which runs in parallel with the "main" user chat lane. --- README.md | 4 ++++ src/core/persona/persona-generator.ts | 3 +++ src/core/record/l1-dedup.ts | 8 ++++++-- src/core/record/l1-extractor.ts | 6 ++++++ src/core/scene/scene-extractor.ts | 6 ++++++ src/utils/clean-context-runner.ts | 15 +++++++++++++++ 6 files changed, 40 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 16f3cc8..148ed18 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,10 @@ docker exec -it hermes-memory hermes | `pipeline.l2MinIntervalSeconds` | `900` | Minimum interval between two L2 passes within the same session | | `recall.timeoutMs` | `5000` | Recall timeout; on timeout, skip injection without blocking the conversation | | `extraction.enableDedup` | `true` | L1 vector dedup / conflict detection | +| `extraction.model` | *(default model)* | LLM model for L1 extraction (format: `provider/model`) | +| `extraction.lane` | *(main lane)* | Global lane for extraction runs (e.g. `"cron"` to use `"cron-nested"` lane) | +| `persona.model` | *(default model)* | LLM model for L2 scene extraction & L3 persona generation | +| `persona.lane` | *(main lane)* | Global lane for persona/scene runs | | `capture.excludeAgents` | `[]` | Glob patterns to exclude specific agents (e.g. `bench-judge-*`) | | `capture.l0l1RetentionDays` | `0` | Local retention days for L0 / L1 files; `0` = never clean up | | `offload.mildOffloadRatio` | `0.5` | Mild compression trigger ratio (of context window) | diff --git a/src/core/persona/persona-generator.ts b/src/core/persona/persona-generator.ts index 438b359..a2bdb6a 100644 --- a/src/core/persona/persona-generator.ts +++ b/src/core/persona/persona-generator.ts @@ -36,6 +36,8 @@ export class PersonaGenerator { config: unknown; model?: string; backupCount?: number; + /** Global lane name for the embedded agent run. */ + lane?: string; logger?: Logger; /** Plugin instance ID for metric reporting (optional) */ instanceId?: string; @@ -54,6 +56,7 @@ export class PersonaGenerator { this.runner = opts.llmRunner ?? new CleanContextRunner({ config: opts.config, modelRef: opts.model, + lane: opts.lane, enableTools: true, logger: opts.logger, }); diff --git a/src/core/record/l1-dedup.ts b/src/core/record/l1-dedup.ts index 5d833e1..ab938f5 100644 --- a/src/core/record/l1-dedup.ts +++ b/src/core/record/l1-dedup.ts @@ -68,10 +68,12 @@ export async function batchDedup(params: { conflictRecallTopK?: number; /** Override embedding timeout for capture-path calls (milliseconds) */ embeddingTimeoutMs?: number; + /** Global lane name for the embedded agent run. */ + lane?: string; /** Host-neutral LLM runner — when provided, used instead of CleanContextRunner. */ llmRunner?: LLMRunner; }): Promise { - const { memories, config, logger, model, vectorStore, embeddingService, llmRunner } = params; + const { memories, config, logger, model, vectorStore, embeddingService, llmRunner, lane } = params; const topK = params.conflictRecallTopK ?? 5; if (memories.length === 0) { @@ -139,7 +141,7 @@ export async function batchDedup(params: { } // Phase 2: Batch LLM judgment - return runLlmJudgment(matches, memories, config, logger, model, llmRunner); + return runLlmJudgment(matches, memories, config, logger, model, lane, llmRunner); } /** @@ -151,6 +153,7 @@ async function runLlmJudgment( config: unknown, logger: Logger | undefined, model: string | undefined, + lane: string | undefined, llmRunner?: LLMRunner, ): Promise { logger?.debug?.(`${TAG} Running batch conflict detection for ${memories.length} memories`); @@ -172,6 +175,7 @@ async function runLlmJudgment( const runner = new CleanContextRunner({ config, modelRef: model, + lane, enableTools: false, logger, }); diff --git a/src/core/record/l1-extractor.ts b/src/core/record/l1-extractor.ts index ad0a8f1..15fc682 100644 --- a/src/core/record/l1-extractor.ts +++ b/src/core/record/l1-extractor.ts @@ -98,6 +98,8 @@ export async function extractL1Memories(params: { model?: string; /** Previous scene name for continuity */ previousSceneName?: string; + /** Global lane name for the embedded agent run. */ + lane?: string; /** Vector store for cosine similarity candidate recall */ vectorStore?: IMemoryStore; /** Embedding service for computing query vectors */ @@ -165,6 +167,7 @@ export async function extractL1Memories(params: { config, logger, model: options.model, + lane: options.lane, llmRunner: options.llmRunner, }); logger?.debug?.(`${TAG} LLM detected ${scenes.length} scene(s)`); @@ -307,6 +310,8 @@ async function callLlmExtraction(params: { config: unknown; logger?: Logger; model?: string; + /** Global lane name for the embedded agent run. */ + lane?: string; /** Host-neutral LLM runner — when provided, used instead of CleanContextRunner. */ llmRunner?: LLMRunner; }): Promise { @@ -338,6 +343,7 @@ async function callLlmExtraction(params: { const runner = new CleanContextRunner({ config, modelRef: model, + lane: params.lane, enableTools: false, logger, }); diff --git a/src/core/scene/scene-extractor.ts b/src/core/scene/scene-extractor.ts index 71288f4..09fd5c2 100644 --- a/src/core/scene/scene-extractor.ts +++ b/src/core/scene/scene-extractor.ts @@ -54,6 +54,11 @@ export interface SceneExtractorOptions { logger?: ExtractorLogger; /** Plugin instance ID for metric reporting (optional) */ instanceId?: string; + /** + * Global lane name for the embedded agent run. + * @default undefined → OpenClaw defaults to "main" + */ + lane?: string; /** * Host-neutral LLM runner. When provided, used instead of creating * a CleanContextRunner (decouples from OpenClaw runtime). @@ -106,6 +111,7 @@ export class SceneExtractor { this.runner = opts.llmRunner ?? new CleanContextRunner({ config: opts.config, modelRef: opts.model, + lane: opts.lane, enableTools: true, logger: opts.logger, }); diff --git a/src/utils/clean-context-runner.ts b/src/utils/clean-context-runner.ts index 868d787..fb623b6 100644 --- a/src/utils/clean-context-runner.ts +++ b/src/utils/clean-context-runner.ts @@ -285,6 +285,18 @@ export interface CleanContextRunnerOptions { agentRuntime?: EmbeddedAgentRuntimeLike; /** Allow the LLM to use tools (read_file, write_to_file, etc). Default: false */ enableTools?: boolean; + /** + * Global lane name for the embedded agent run. + * + * OpenClaw routes embedded runs through a session lane (one-at-a-time per session) + * and a global lane (shared parallelism pool). By default the global lane is `"main"`, + * which is also used by the primary chat agent. Setting this to `"cron"` (→ `"cron-nested"`) + * or another named lane prevents long-running extraction calls from competing with + * interactive chat turns. + * + * @default undefined → OpenClaw defaults to `"main"` + */ + lane?: string; /** Logger instance for detailed tracing */ logger?: RunnerLogger; } @@ -459,6 +471,9 @@ export class CleanContextRunner { // providers (qwencode) reject with "[] is too short - 'tools'". // Instead rely on cleanConfig.tools.allow to restrict the tool set // to a minimal read-only tool (when enableTools=false). + // Pass lane to use a dedicated global lane (e.g. "cron" → "cron-nested") + // instead of the default "main" lane shared with interactive sessions. + lane: this.options.lane, disableTools: false, extraSystemPrompt: effectiveSystemPrompt, streamParams: {