feat: add orchestrator execution engine, observability panel, and LSP document caching

Wire FlowExecutor into orchestrator routes for actual flow execution with
pause/resume/stop lifecycle management. Add CLI session audit system with
audit-routes backend and Observability tab in IssueHub frontend. Introduce
cli-session-mux for cross-workspace session routing and QueueSendToOrchestrator
UI component. Normalize frontend API response handling for { data: ... }
wrapper format and propagate projectPath through flow hooks.

In codex-lens, add per-server opened-document cache in StandaloneLspManager
to avoid redundant didOpen notifications (using didChange for updates), and
skip warmup delay for already-warmed LSP server instances in ChainSearchEngine.
This commit is contained in:
catlog22
2026-02-11 15:38:33 +08:00
parent d0cdee2e68
commit 5a9e54fd70
35 changed files with 5325 additions and 77 deletions

View File

@@ -177,6 +177,14 @@ export class CliSessionManager {
return Array.from(this.sessions.values()).map(({ pty: _pty, buffer: _buffer, bufferBytes: _bytes, ...rest }) => rest);
}
getProjectRoot(): string {
return this.projectRoot;
}
hasSession(sessionKey: string): boolean {
return this.sessions.has(sessionKey);
}
getSession(sessionKey: string): CliSession | null {
const session = this.sessions.get(sessionKey);
if (!session) return null;
@@ -398,3 +406,15 @@ export function getCliSessionManager(projectRoot: string = process.cwd()): CliSe
managersByRoot.set(resolved, created);
return created;
}
/**
* Find the manager that owns a given sessionKey.
* Useful for cross-workspace routing (tmux-like send) where the executor
* may not share the same workflowDir/projectRoot as the target session.
*/
export function findCliSessionManager(sessionKey: string): CliSessionManager | null {
for (const manager of managersByRoot.values()) {
if (manager.hasSession(sessionKey)) return manager;
}
return null;
}

View File

@@ -0,0 +1,24 @@
/**
* CliSessionMux
*
* A tiny indirection layer used by FlowExecutor (and potentially others) to
* route commands to existing PTY sessions in a testable way.
*
* Why this exists:
* - ESM module namespace exports are immutable, which makes it hard to mock
* named exports in node:test without special loaders.
* - Exporting a mutable object lets tests override behavior by swapping
* functions on the object.
*/
import type { CliSessionManager } from './cli-session-manager.js';
import { findCliSessionManager, getCliSessionManager } from './cli-session-manager.js';
export const cliSessionMux: {
findCliSessionManager: (sessionKey: string) => CliSessionManager | null;
getCliSessionManager: (projectRoot?: string) => CliSessionManager;
} = {
findCliSessionManager,
getCliSessionManager,
};

View File

@@ -19,7 +19,8 @@ import { existsSync } from 'fs';
import { join } from 'path';
import { broadcastToClients } from '../websocket.js';
import { executeCliTool } from '../../tools/cli-executor-core.js';
import { getCliSessionManager } from './cli-session-manager.js';
import { cliSessionMux } from './cli-session-mux.js';
import { appendCliSessionAudit } from './cli-session-audit.js';
import type {
Flow,
FlowNode,
@@ -255,16 +256,46 @@ export class NodeRunner {
};
}
const manager = getCliSessionManager(this.context.workingDir || process.cwd());
const manager = cliSessionMux.findCliSessionManager(targetSessionKey)
?? cliSessionMux.getCliSessionManager(this.context.workingDir || process.cwd());
if (!manager.hasSession(targetSessionKey)) {
return {
success: false,
error: `Target session not found: ${targetSessionKey}`
};
}
const routed = manager.execute(targetSessionKey, {
tool,
prompt: instruction,
mode,
workingDir: this.context.workingDir,
resumeKey: data.resumeKey,
resumeStrategy: data.resumeStrategy === 'promptConcat' ? 'promptConcat' : 'nativeResume'
});
// Best-effort: record audit event so Observability panel includes orchestrator-routed executions.
try {
const session = manager.getSession(targetSessionKey);
appendCliSessionAudit({
type: 'session_execute',
timestamp: new Date().toISOString(),
projectRoot: manager.getProjectRoot(),
sessionKey: targetSessionKey,
tool,
resumeKey: data.resumeKey,
workingDir: session?.workingDir,
details: {
executionId: routed.executionId,
mode,
resumeStrategy: data.resumeStrategy ?? 'nativeResume',
delivery: 'sendToSession',
flowId: this.context.flowId,
nodeId: node.id
}
});
} catch {
// ignore
}
const outputKey = data.outputName || `${node.id}_output`;
this.context.variables[outputKey] = {
delivery: 'sendToSession',