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:
Tam Nhu Tran
2026-04-30 13:00:49 -04:00
parent 1ff4329ee4
commit 8afee344e1
4 changed files with 70 additions and 0 deletions
+10
View File
@@ -13,6 +13,9 @@ import ProfileContextSyncLock from './profile-context-sync-lock';
import { DEFAULT_ACCOUNT_CONTEXT_MODE } from '../auth/account-context';
import type { AccountContextPolicy } from '../auth/account-context';
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']);
@@ -52,6 +55,10 @@ class InstanceManager {
await this.contextSyncLock.withLock(profileName, async () => {
// Lazy initialization
if (!fs.existsSync(instancePath)) {
logger.stage('route', 'instance.init', 'Initializing new profile instance', {
profile: profileName,
bare: options.bare === true,
});
this.initializeInstance(profileName, instancePath, options);
}
@@ -170,6 +177,9 @@ class InstanceManager {
}
fs.rmSync(instancePath, { recursive: true, force: true });
logger.stage('cleanup', 'instance.deleted', 'Profile instance deleted', {
profile: profileName,
});
});
});
}
+20
View File
@@ -21,6 +21,9 @@ import { getWebSearchHookEnv } from '../utils/websearch-manager';
import { appendBrowserToolArgs } from '../utils/browser';
import { wireChildProcessSignals } from '../utils/signal-forwarder';
import { runCleanup } from '../errors';
import { createLogger } from '../services/logging';
const adapterLogger = createLogger('targets:claude');
export class ClaudeAdapter implements TargetAdapter {
readonly type: TargetType = 'claude';
@@ -101,6 +104,13 @@ export class ClaudeAdapter implements TargetAdapter {
const isPowerShellScript = isWindows && /\.ps1$/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;
if (isPowerShellScript) {
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) => {
if (err.code === 'EACCES') {
console.error(`[X] Claude CLI is not executable: ${claudeCli}`);
+20
View File
@@ -23,6 +23,9 @@ import {
getCodexBinaryInfo,
readCodexVersion,
} from './codex-detector';
import { createLogger } from '../services/logging';
const adapterLogger = createLogger('targets:codex');
const CODEX_RUNTIME_PROVIDER_ID = 'ccs_runtime';
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 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;
if (isPowerShellScript) {
child = spawn(
@@ -329,6 +339,16 @@ export class CodexAdapter implements TargetAdapter {
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) => {
if (err.code === 'EACCES') {
console.error(`[X] Codex CLI is not executable: ${codexPath}`);
+20
View File
@@ -19,6 +19,9 @@ import {
} from '../utils/shell-executor';
import { wireChildProcessSignals } from '../utils/signal-forwarder';
import { runCleanup } from '../errors';
import { createLogger } from '../services/logging';
const adapterLogger = createLogger('targets:droid');
export class DroidAdapter implements TargetAdapter {
readonly type: TargetType = 'droid';
@@ -122,6 +125,13 @@ export class DroidAdapter implements TargetAdapter {
const isPowerShellScript = isWindows && /\.ps1$/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;
if (isPowerShellScript) {
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) => {
if (err.code === 'EACCES') {
console.error(`[X] Droid CLI is not executable: ${droidPath}`);