mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 22:17:14 +00:00
feat(targets,management): instrument adapter spawn lifecycle and instance manager
Each target adapter (claude, codex, droid) now emits dispatch on child spawn and respond on child exit with latencyMs. instance-manager emits lifecycle events for instance create / start / stop so multi-instance flows are traceable per requestId. Refs #1141, #1138
This commit is contained in:
@@ -13,6 +13,9 @@ import ProfileContextSyncLock from './profile-context-sync-lock';
|
|||||||
import { DEFAULT_ACCOUNT_CONTEXT_MODE } from '../auth/account-context';
|
import { DEFAULT_ACCOUNT_CONTEXT_MODE } from '../auth/account-context';
|
||||||
import type { AccountContextPolicy } from '../auth/account-context';
|
import type { AccountContextPolicy } from '../auth/account-context';
|
||||||
import { getCcsDir, getCcsHome } from '../utils/config-manager';
|
import { getCcsDir, getCcsHome } from '../utils/config-manager';
|
||||||
|
import { createLogger } from '../services/logging';
|
||||||
|
|
||||||
|
const logger = createLogger('management:instance-manager');
|
||||||
|
|
||||||
const MANAGED_MCP_SERVER_NAMES = new Set(['ccs-websearch', 'ccs-image-analysis', 'ccs-browser']);
|
const MANAGED_MCP_SERVER_NAMES = new Set(['ccs-websearch', 'ccs-image-analysis', 'ccs-browser']);
|
||||||
|
|
||||||
@@ -52,6 +55,10 @@ class InstanceManager {
|
|||||||
await this.contextSyncLock.withLock(profileName, async () => {
|
await this.contextSyncLock.withLock(profileName, async () => {
|
||||||
// Lazy initialization
|
// Lazy initialization
|
||||||
if (!fs.existsSync(instancePath)) {
|
if (!fs.existsSync(instancePath)) {
|
||||||
|
logger.stage('route', 'instance.init', 'Initializing new profile instance', {
|
||||||
|
profile: profileName,
|
||||||
|
bare: options.bare === true,
|
||||||
|
});
|
||||||
this.initializeInstance(profileName, instancePath, options);
|
this.initializeInstance(profileName, instancePath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +177,9 @@ class InstanceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.rmSync(instancePath, { recursive: true, force: true });
|
fs.rmSync(instancePath, { recursive: true, force: true });
|
||||||
|
logger.stage('cleanup', 'instance.deleted', 'Profile instance deleted', {
|
||||||
|
profile: profileName,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import { getWebSearchHookEnv } from '../utils/websearch-manager';
|
|||||||
import { appendBrowserToolArgs } from '../utils/browser';
|
import { appendBrowserToolArgs } from '../utils/browser';
|
||||||
import { wireChildProcessSignals } from '../utils/signal-forwarder';
|
import { wireChildProcessSignals } from '../utils/signal-forwarder';
|
||||||
import { runCleanup } from '../errors';
|
import { runCleanup } from '../errors';
|
||||||
|
import { createLogger } from '../services/logging';
|
||||||
|
|
||||||
|
const adapterLogger = createLogger('targets:claude');
|
||||||
|
|
||||||
export class ClaudeAdapter implements TargetAdapter {
|
export class ClaudeAdapter implements TargetAdapter {
|
||||||
readonly type: TargetType = 'claude';
|
readonly type: TargetType = 'claude';
|
||||||
@@ -101,6 +104,13 @@ export class ClaudeAdapter implements TargetAdapter {
|
|||||||
const isPowerShellScript = isWindows && /\.ps1$/i.test(claudeCli);
|
const isPowerShellScript = isWindows && /\.ps1$/i.test(claudeCli);
|
||||||
const needsShell = isWindows && /\.(cmd|bat)$/i.test(claudeCli);
|
const needsShell = isWindows && /\.(cmd|bat)$/i.test(claudeCli);
|
||||||
|
|
||||||
|
const spawnStartedAt = Date.now();
|
||||||
|
adapterLogger.stage('dispatch', 'target.spawn', 'Spawning Claude CLI child process', {
|
||||||
|
target: 'claude',
|
||||||
|
claudeCli,
|
||||||
|
argCount: args.length,
|
||||||
|
});
|
||||||
|
|
||||||
let child: ChildProcess;
|
let child: ChildProcess;
|
||||||
if (isPowerShellScript) {
|
if (isPowerShellScript) {
|
||||||
child = spawn(
|
child = spawn(
|
||||||
@@ -128,6 +138,16 @@ export class ClaudeAdapter implements TargetAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
adapterLogger.stage(
|
||||||
|
'respond',
|
||||||
|
'target.exit',
|
||||||
|
'Claude CLI child process exited',
|
||||||
|
{ target: 'claude', exitCode: code, signal },
|
||||||
|
{ latencyMs: Date.now() - spawnStartedAt }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
wireChildProcessSignals(child, async (err: NodeJS.ErrnoException) => {
|
wireChildProcessSignals(child, async (err: NodeJS.ErrnoException) => {
|
||||||
if (err.code === 'EACCES') {
|
if (err.code === 'EACCES') {
|
||||||
console.error(`[X] Claude CLI is not executable: ${claudeCli}`);
|
console.error(`[X] Claude CLI is not executable: ${claudeCli}`);
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import {
|
|||||||
getCodexBinaryInfo,
|
getCodexBinaryInfo,
|
||||||
readCodexVersion,
|
readCodexVersion,
|
||||||
} from './codex-detector';
|
} from './codex-detector';
|
||||||
|
import { createLogger } from '../services/logging';
|
||||||
|
|
||||||
|
const adapterLogger = createLogger('targets:codex');
|
||||||
|
|
||||||
const CODEX_RUNTIME_PROVIDER_ID = 'ccs_runtime';
|
const CODEX_RUNTIME_PROVIDER_ID = 'ccs_runtime';
|
||||||
const CODEX_RUNTIME_ENV_KEY = 'CCS_CODEX_API_KEY';
|
const CODEX_RUNTIME_ENV_KEY = 'CCS_CODEX_API_KEY';
|
||||||
@@ -310,6 +313,13 @@ export class CodexAdapter implements TargetAdapter {
|
|||||||
const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath);
|
const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath);
|
||||||
const needsShell = isWindows && /\.(cmd|bat)$/i.test(codexPath);
|
const needsShell = isWindows && /\.(cmd|bat)$/i.test(codexPath);
|
||||||
|
|
||||||
|
const spawnStartedAt = Date.now();
|
||||||
|
adapterLogger.stage('dispatch', 'target.spawn', 'Spawning Codex CLI child process', {
|
||||||
|
target: 'codex',
|
||||||
|
codexPath,
|
||||||
|
argCount: args.length,
|
||||||
|
});
|
||||||
|
|
||||||
let child: ChildProcess;
|
let child: ChildProcess;
|
||||||
if (isPowerShellScript) {
|
if (isPowerShellScript) {
|
||||||
child = spawn(
|
child = spawn(
|
||||||
@@ -329,6 +339,16 @@ export class CodexAdapter implements TargetAdapter {
|
|||||||
child = spawn(codexPath, args, { stdio: 'inherit', windowsHide: true, env: launchEnv });
|
child = spawn(codexPath, args, { stdio: 'inherit', windowsHide: true, env: launchEnv });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
adapterLogger.stage(
|
||||||
|
'respond',
|
||||||
|
'target.exit',
|
||||||
|
'Codex CLI child process exited',
|
||||||
|
{ target: 'codex', exitCode: code, signal },
|
||||||
|
{ latencyMs: Date.now() - spawnStartedAt }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
wireChildProcessSignals(child, (err: NodeJS.ErrnoException) => {
|
wireChildProcessSignals(child, (err: NodeJS.ErrnoException) => {
|
||||||
if (err.code === 'EACCES') {
|
if (err.code === 'EACCES') {
|
||||||
console.error(`[X] Codex CLI is not executable: ${codexPath}`);
|
console.error(`[X] Codex CLI is not executable: ${codexPath}`);
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import {
|
|||||||
} from '../utils/shell-executor';
|
} from '../utils/shell-executor';
|
||||||
import { wireChildProcessSignals } from '../utils/signal-forwarder';
|
import { wireChildProcessSignals } from '../utils/signal-forwarder';
|
||||||
import { runCleanup } from '../errors';
|
import { runCleanup } from '../errors';
|
||||||
|
import { createLogger } from '../services/logging';
|
||||||
|
|
||||||
|
const adapterLogger = createLogger('targets:droid');
|
||||||
|
|
||||||
export class DroidAdapter implements TargetAdapter {
|
export class DroidAdapter implements TargetAdapter {
|
||||||
readonly type: TargetType = 'droid';
|
readonly type: TargetType = 'droid';
|
||||||
@@ -122,6 +125,13 @@ export class DroidAdapter implements TargetAdapter {
|
|||||||
const isPowerShellScript = isWindows && /\.ps1$/i.test(droidPath);
|
const isPowerShellScript = isWindows && /\.ps1$/i.test(droidPath);
|
||||||
const needsShell = isWindows && /\.(cmd|bat)$/i.test(droidPath);
|
const needsShell = isWindows && /\.(cmd|bat)$/i.test(droidPath);
|
||||||
|
|
||||||
|
const spawnStartedAt = Date.now();
|
||||||
|
adapterLogger.stage('dispatch', 'target.spawn', 'Spawning Droid CLI child process', {
|
||||||
|
target: 'droid',
|
||||||
|
droidPath,
|
||||||
|
argCount: args.length,
|
||||||
|
});
|
||||||
|
|
||||||
let child: ChildProcess;
|
let child: ChildProcess;
|
||||||
if (isPowerShellScript) {
|
if (isPowerShellScript) {
|
||||||
child = spawn(
|
child = spawn(
|
||||||
@@ -149,6 +159,16 @@ export class DroidAdapter implements TargetAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
adapterLogger.stage(
|
||||||
|
'respond',
|
||||||
|
'target.exit',
|
||||||
|
'Droid CLI child process exited',
|
||||||
|
{ target: 'droid', exitCode: code, signal },
|
||||||
|
{ latencyMs: Date.now() - spawnStartedAt }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
wireChildProcessSignals(child, (err: NodeJS.ErrnoException) => {
|
wireChildProcessSignals(child, (err: NodeJS.ErrnoException) => {
|
||||||
if (err.code === 'EACCES') {
|
if (err.code === 'EACCES') {
|
||||||
console.error(`[X] Droid CLI is not executable: ${droidPath}`);
|
console.error(`[X] Droid CLI is not executable: ${droidPath}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user