Files
Codeman/src/mux-interface.ts
T
Tenggan ZhangandTeigen b2f8b03b3c feat: inject effort as soft default via CLI flags instead of env var (#104)
CLAUDE_CODE_EFFORT_LEVEL hard-locks effort for the whole session and makes
Claude reject in-session /effort switching (incl. ultracode). Carry effort
as a dedicated payload field instead, injected at spawn as a soft default:

- regular levels (incl. max) -> claude --effort <level>
  (the settings effortLevel key is enum([low,medium,high,xhigh]) with
  .catch(undefined), so max would be silently dropped there)
- ultracode -> claude --settings '{"ultracode":true}'
  (dedicated boolean settings key, rejected by the --effort flag)

Changes:
- add effort enum field to create/quick-start/ralph-loop schemas and thread
  it through Session -> CreateSessionOptions/RespawnPaneOptions -> spawn
- buildEffortCliArgs() in session-cli-builder, shared by tmux spawn command
  and direct-PTY fallback args
- frontend: buildEnvOverrides() no longer emits CLAUDE_CODE_EFFORT_LEVEL;
  validated effort goes into payloads via getEffortSetting()
- settings UI: add Ultracode option to the Thinking Effort dropdown
- legacy migration: Session constructor extracts CLAUDE_CODE_EFFORT_LEVEL
  from persisted envOverrides; applyEnvOverrides() unsets the stale tmux
  session var so respawned panes are no longer locked
- tests: test/effort-injection.test.ts (13 cases)

Co-authored-by: Teigen <teigenzhang@gmail.com>
2026-06-07 04:33:11 +02:00

209 lines
6.3 KiB
TypeScript

/**
* @fileoverview Terminal multiplexer abstraction layer (tmux).
*
* Defines the TerminalMultiplexer interface that TmuxManager implements.
*
* @module mux-interface
*/
import type { EventEmitter } from 'node:events';
import type {
ProcessStats,
PersistedRespawnConfig,
NiceConfig,
ClaudeMode,
SessionMode,
OpenCodeConfig,
EffortLevel,
} from './types.js';
/**
* Multiplexer session metadata.
*/
export interface MuxSession {
/** Codeman session ID */
sessionId: string;
/** Multiplexer session name (e.g., "codeman-abc12345") */
muxName: string;
/** Process PID */
pid: number;
/** Timestamp when created */
createdAt: number;
/** Working directory */
workingDir: string;
/** Session mode */
mode: SessionMode;
/** Whether webserver is attached to this session */
attached: boolean;
/** Session display name (tab name) */
name?: string;
/** Persisted respawn controller configuration (restored on server restart) */
respawnConfig?: PersistedRespawnConfig;
/** Whether Ralph / Todo tracking is enabled */
ralphEnabled?: boolean;
}
/**
* MuxSession with optional process resource statistics.
*/
export interface MuxSessionWithStats extends MuxSession {
/** Optional resource statistics */
stats?: ProcessStats;
}
/** Options for creating a new multiplexer session. */
export interface CreateSessionOptions {
sessionId: string;
workingDir: string;
mode: SessionMode;
name?: string;
niceConfig?: NiceConfig;
model?: string;
claudeMode?: ClaudeMode;
allowedTools?: string;
openCodeConfig?: OpenCodeConfig;
/** When restoring after reboot, resume a previous Claude conversation by its session ID */
resumeSessionId?: string;
/** Extra env vars exported before launching the CLI (e.g., CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS). Ephemeral — not written to disk. */
envOverrides?: Record<string, string>;
/** Claude CLI effort level, injected as a `--settings` soft default (overridable via /effort in-session) */
effort?: EffortLevel;
}
/** Options for respawning a dead pane. */
export interface RespawnPaneOptions {
sessionId: string;
workingDir: string;
mode: SessionMode;
niceConfig?: NiceConfig;
model?: string;
claudeMode?: ClaudeMode;
allowedTools?: string;
openCodeConfig?: OpenCodeConfig;
/** Resume a previous Claude conversation when respawning */
resumeSessionId?: string;
/** Extra env vars exported before launching the CLI (preserved across respawns). */
envOverrides?: Record<string, string>;
/** Claude CLI effort level (preserved across respawns, injected via `--settings`) */
effort?: EffortLevel;
}
/**
* Terminal multiplexer interface.
*
* Implemented by TmuxManager.
*
* Events emitted:
* - `sessionCreated` (session: MuxSession) - New session created
* - `sessionKilled` (data: { sessionId: string }) - Session terminated
* - `sessionDied` (data: { sessionId: string }) - Session died unexpectedly
* - `statsUpdated` (sessions: MuxSessionWithStats[]) - Stats refreshed
*/
export interface TerminalMultiplexer extends EventEmitter {
/** Which backend this instance uses */
readonly backend: 'tmux';
/** The dedicated tmux socket name all sessions live on (e.g. "codeman"). */
readonly muxSocket: string;
// ========== Lifecycle ==========
/**
* Create a new multiplexer session.
* The session runs the appropriate command (claude, opencode, or shell) in detached mode.
*/
createSession(options: CreateSessionOptions): Promise<MuxSession>;
/**
* Kill a session and all its child processes.
* Uses a multi-strategy approach (children → process group → mux kill → SIGKILL).
*/
killSession(sessionId: string): Promise<boolean>;
/** Clean up resources (stop stats collection, etc.) */
destroy(): void;
// ========== Queries ==========
/** Get all tracked sessions */
getSessions(): MuxSession[];
/** Get a session by Codeman session ID */
getSession(sessionId: string): MuxSession | undefined;
/** Get all sessions with process resource statistics */
getSessionsWithStats(): Promise<MuxSessionWithStats[]>;
/** Get process stats for a single session */
getProcessStats(sessionId: string): Promise<ProcessStats | null>;
// ========== Input ==========
/**
* Send input to a session via tmux send-keys.
*/
sendInput(sessionId: string, input: string): Promise<boolean>;
// ========== Metadata ==========
/** Update the display name of a session */
updateSessionName(sessionId: string, name: string): boolean;
/** Mark session as attached/detached */
setAttached(sessionId: string, attached: boolean): void;
/** Register an externally-created session for tracking */
registerSession(session: MuxSession): void;
/** Update persisted respawn config for a session */
updateRespawnConfig(sessionId: string, config: PersistedRespawnConfig | undefined): void;
/** Clear respawn config when respawn is stopped */
clearRespawnConfig(sessionId: string): void;
/** Update Ralph enabled state for a session */
updateRalphEnabled(sessionId: string, enabled: boolean): void;
// ========== Discovery ==========
/**
* Reconcile tracked sessions with actual running sessions.
* Finds dead sessions and discovers unknown ones.
*/
reconcileSessions(): Promise<{ alive: string[]; dead: string[]; discovered: string[] }>;
// ========== Stats Collection ==========
/** Start periodic process stats collection */
startStatsCollection(intervalMs?: number): void;
/** Stop periodic process stats collection */
stopStatsCollection(): void;
// ========== PTY Attachment ==========
/**
* Get the command to spawn for attaching to a session ('tmux').
*/
getAttachCommand(): string;
/**
* Get the arguments for attaching to a session by mux name.
*/
getAttachArgs(muxName: string): string[];
// ========== Availability ==========
/** Check if the multiplexer binary is available on the system */
isAvailable(): boolean;
/** Check if a multiplexer session actually exists (process-level check, not just tracked) */
muxSessionExists(muxName: string): boolean;
/** Check if the pane in a session is dead (command exited but remain-on-exit keeps it alive) */
isPaneDead(muxName: string): boolean;
/** Respawn a dead pane with a fresh command. Returns the new PID or null on failure. */
respawnPane(options: RespawnPaneOptions): Promise<number | null>;
}