wip: uncommitted changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2026-01-03 16:06:40 +07:00
co-authored by Claude Opus 4.5
parent 4185db9c75
commit e8890a2fc1
21 changed files with 694 additions and 1053 deletions
@@ -89,7 +89,10 @@ class TestAgentSession implements AgentSession {
async respondToPermission(): Promise<void> {}
describePersistence() {
return null;
return {
provider: this.provider,
sessionId: this.id,
};
}
async interrupt(): Promise<void> {}
@@ -165,7 +168,7 @@ describe("AgentManager", () => {
expect(snapshot.runtimeInfo).toBeDefined();
expect(snapshot.runtimeInfo?.model).toBe("gpt-5.2-codex");
expect(snapshot.runtimeInfo?.sessionId).toBe(snapshot.sessionId);
expect(snapshot.runtimeInfo?.sessionId).toBe(snapshot.persistence?.sessionId);
});
test("runAgent refreshes runtimeInfo after completion", async () => {
@@ -93,7 +93,6 @@ type ManagedAgentBase = {
persistence: AgentPersistenceHandle | null;
historyPrimed: boolean;
lastUserMessageAt: Date | null;
sessionId: string | null;
lastUsage?: AgentUsage;
lastError?: string;
attention: AttentionState;
@@ -368,7 +367,6 @@ export class AgentManager {
lifecycle: "closed",
session: null,
pendingRun: null,
sessionId: agent.sessionId,
};
await session.close();
this.emitState(closedAgent);
@@ -421,8 +419,14 @@ export class AgentManager {
}
const agent = this.requireAgent(agentId);
const sessionId = agent.persistence?.sessionId;
if (!sessionId) {
throw new Error(
`Agent ${agentId} has no persistence.sessionId after run completed`
);
}
return {
sessionId: agent.sessionId ?? agent.id,
sessionId,
finalText,
usage,
timeline,
@@ -767,7 +771,6 @@ export class AgentManager {
provider: config.provider,
cwd: config.cwd,
session,
sessionId: session.id,
capabilities: session.capabilities,
config,
runtimeInfo: undefined,
@@ -878,7 +881,9 @@ export class AgentManager {
switch (event.type) {
case "thread_started":
agent.sessionId = event.sessionId;
// Update persistence with the new session ID from the provider.
// persistence.sessionId is the single source of truth for session identity.
agent.persistence = agent.session.describePersistence();
break;
case "timeline":
this.recordTimeline(agent, event.item);
@@ -407,7 +407,7 @@ describe("agent MCP end-to-end", () => {
cwd: agentCwd,
title: "MCP interrupt test",
agentType: "codex",
initialMode: "full-auto",
initialMode: "full-access",
background: true, // Start in background so create returns immediately
},
})) as McpToolResult;
@@ -234,7 +234,6 @@ describe("toAgentPayload", () => {
expect(payload.lastUserMessageAt).toBe(agent.lastUserMessageAt?.toISOString());
expect(payload.title).toBe("UI Payload");
expect(payload.model).toBe(agent.config.model);
expect(payload.sessionId).toBe(agent.sessionId);
expect(payload.pendingPermissions.map((item) => item.id)).toEqual([
"perm-a",
"perm-b",
@@ -77,7 +77,6 @@ export function toAgentPayload(
? agent.lastUserMessageAt.toISOString()
: null,
status: agent.lifecycle,
sessionId: agent.sessionId,
capabilities: cloneCapabilities(agent.capabilities),
currentModeId: agent.currentModeId,
availableModes: cloneAvailableModes(agent.availableModes),
@@ -51,7 +51,6 @@ function createManagedAgent(
provider,
cwd,
session,
sessionId: overrides.sessionId ?? "session-123",
capabilities:
overrides.capabilities ??
{
@@ -66,6 +66,7 @@ export class AgentRegistry {
private cache: Map<string, StoredAgentRecord> = new Map();
private loaded = false;
private filePath: string;
private loadPromise: Promise<StoredAgentRecord[]> | null = null;
constructor(filePath: string) {
this.filePath = filePath;
@@ -75,6 +76,15 @@ export class AgentRegistry {
if (this.loaded) {
return Array.from(this.cache.values());
}
if (!this.loadPromise) {
this.loadPromise = this.doLoad();
}
return this.loadPromise;
}
private async doLoad(): Promise<StoredAgentRecord[]> {
try {
const content = await fs.readFile(this.filePath, "utf8");
const parsed = await this.parseContent(content);
+185 -68
View File
@@ -36,6 +36,47 @@ export interface AgentMcpServerOptions {
callerAgentId?: string;
}
const CLAUDE_TO_CODEX_MODE: Record<string, string> = {
plan: "read-only",
default: "auto",
acceptEdits: "auto",
bypassPermissions: "full-access",
};
const CODEX_TO_CLAUDE_MODE: Record<string, string> = {
"read-only": "plan",
auto: "default",
"full-access": "bypassPermissions",
};
function mapModeAcrossProviders(
sourceMode: string,
sourceProvider: AgentProvider,
targetProvider: AgentProvider
): string {
if (sourceProvider === targetProvider) {
return sourceMode;
}
if (sourceProvider === "claude" && targetProvider === "codex") {
const mapped = CLAUDE_TO_CODEX_MODE[sourceMode];
if (mapped) {
return mapped;
}
return "auto";
}
if (sourceProvider === "codex" && targetProvider === "claude") {
const mapped = CODEX_TO_CLAUDE_MODE[sourceMode];
if (mapped) {
return mapped;
}
return "default";
}
return sourceMode;
}
const AgentProviderEnum = z.enum(
AGENT_PROVIDER_DEFINITIONS.map((definition) => definition.id) as [
AgentProvider,
@@ -138,59 +179,91 @@ export async function createAgentMcpServer(
version: "2.0.0",
});
const agentToAgentInputSchema = {
title: z
.string()
.trim()
.min(1, "Title is required")
.max(40, "Title must be 40 characters or fewer")
.describe(
"Short descriptive title (<= 40 chars) summarizing the agent's focus. Use a single concise sentence that fits on mobile."
),
agentType: AgentProviderEnum.optional().describe(
"Optional agent implementation to spawn. Defaults to 'claude'."
),
initialPrompt: z
.string()
.optional()
.describe(
"Optional task to start immediately after creation (non-blocking)."
),
background: z
.boolean()
.optional()
.default(false)
.describe(
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately."
),
};
const topLevelInputSchema = {
cwd: z
.string()
.describe(
"Required working directory for the agent (absolute, relative, or ~)."
),
title: z
.string()
.trim()
.min(1, "Title is required")
.max(40, "Title must be 40 characters or fewer")
.describe(
"Short descriptive title (<= 40 chars) summarizing the agent's focus. Use a single concise sentence that fits on mobile."
),
agentType: AgentProviderEnum.optional().describe(
"Optional agent implementation to spawn. Defaults to 'claude'."
),
initialPrompt: z
.string()
.optional()
.describe(
"Optional task to start immediately after creation (non-blocking)."
),
initialMode: z
.string()
.describe("Required session mode to configure before the first run."),
worktreeName: z
.string()
.optional()
.describe(
"Optional git worktree branch name (lowercase alphanumerics + hyphen)."
),
background: z
.boolean()
.optional()
.default(false)
.describe(
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately."
),
parentAgentId: z
.string()
.optional()
.describe(
"Optional parent agent ID. When set, this agent is a child of the specified parent agent."
),
};
const createAgentInputSchema = callerAgentId
? agentToAgentInputSchema
: topLevelInputSchema;
server.registerTool(
"create_agent",
{
title: "Create Agent",
description:
"Create a new Claude or Codex agent tied to a working directory. Optionally run an initial prompt immediately or create a git worktree for the agent.",
inputSchema: {
cwd: z
.string()
.describe(
"Required working directory for the agent (absolute, relative, or ~)."
),
title: z
.string()
.trim()
.min(1, "Title is required")
.max(40, "Title must be 40 characters or fewer")
.describe(
"Short descriptive title (<= 40 chars) summarizing the agent's focus. Use a single concise sentence that fits on mobile."
),
agentType: AgentProviderEnum.optional().describe(
"Optional agent implementation to spawn. Defaults to 'claude'."
),
initialPrompt: z
.string()
.optional()
.describe(
"Optional task to start immediately after creation (non-blocking)."
),
initialMode: z
.string()
.optional()
.describe("Optional session mode to configure before the first run."),
worktreeName: z
.string()
.optional()
.describe(
"Optional git worktree branch name (lowercase alphanumerics + hyphen)."
),
background: z
.boolean()
.optional()
.default(false)
.describe(
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately."
),
parentAgentId: z
.string()
.optional()
.describe(
"Optional parent agent ID. When set, this agent is a child of the specified parent agent."
),
},
inputSchema: createAgentInputSchema,
outputSchema: {
agentId: z.string(),
type: AgentProviderEnum,
@@ -208,35 +281,79 @@ export async function createAgentMcpServer(
permission: AgentPermissionRequestPayloadSchema.nullable().optional(),
},
},
async ({
cwd,
agentType,
initialPrompt,
initialMode,
worktreeName,
background = false,
title,
parentAgentId,
}) => {
let resolvedCwd = expandPath(cwd);
async (args) => {
const {
agentType,
initialPrompt,
background = false,
title,
} = args as {
cwd?: string;
agentType?: AgentProvider;
initialPrompt?: string;
initialMode?: string;
worktreeName?: string;
background?: boolean;
title: string;
parentAgentId?: string;
};
if (worktreeName) {
const worktree = await createWorktree({
branchName: worktreeName,
cwd: resolvedCwd,
worktreeSlug: worktreeName,
});
resolvedCwd = worktree.worktreePath;
let resolvedCwd: string;
let resolvedMode: string | undefined;
let resolvedParentAgentId: string | undefined;
if (callerAgentId) {
const parentAgent = agentManager.getAgent(callerAgentId);
if (!parentAgent) {
throw new Error(`Parent agent ${callerAgentId} not found`);
}
resolvedCwd = parentAgent.cwd;
resolvedParentAgentId = callerAgentId;
const provider: AgentProvider = agentType ?? "claude";
const parentMode = parentAgent.currentModeId;
if (parentMode) {
resolvedMode = mapModeAcrossProviders(
parentMode,
parentAgent.provider,
provider
);
}
} else {
const topLevelArgs = args as unknown as {
cwd: string;
initialMode: string;
worktreeName?: string;
parentAgentId?: string;
};
const {
cwd,
initialMode,
worktreeName,
parentAgentId,
} = topLevelArgs;
resolvedCwd = expandPath(cwd);
if (worktreeName) {
const worktree = await createWorktree({
branchName: worktreeName,
cwd: resolvedCwd,
worktreeSlug: worktreeName,
});
resolvedCwd = worktree.worktreePath;
}
resolvedMode = initialMode;
resolvedParentAgentId = parentAgentId;
}
const provider: AgentProvider = agentType ?? "claude";
const normalizedTitle = title?.trim() ?? null;
// Use explicit parentAgentId if provided, otherwise default to caller agent ID
const resolvedParentAgentId = parentAgentId ?? callerAgentId;
const snapshot = await agentManager.createAgent({
provider,
cwd: resolvedCwd,
modeId: initialMode,
modeId: resolvedMode,
title: normalizedTitle ?? undefined,
parentAgentId: resolvedParentAgentId,
});
@@ -728,6 +728,12 @@ class ClaudeAgentSession implements AgentSession {
stderr: (data: string) => {
console.error("[ClaudeAgentSDK]", data.trim());
},
env: {
...process.env,
// Increase MCP timeouts for long-running tool calls (10 minutes)
MCP_TIMEOUT: "600000",
MCP_TOOL_TIMEOUT: "600000",
},
...this.config.extra?.claude,
};
@@ -117,6 +117,15 @@ const MODE_PRESETS: Record<
},
};
function validateCodexMode(modeId: string): void {
if (!(modeId in MODE_PRESETS)) {
const validModes = Object.keys(MODE_PRESETS).join(", ");
throw new Error(
`Invalid Codex mode "${modeId}". Valid modes are: ${validModes}`
);
}
}
function createToolCallTimelineItem(
data: Omit<ToolCallTimelineItem, "type">
): AgentTimelineItem {
@@ -2757,9 +2766,13 @@ class CodexMcpAgentSession implements AgentSession {
private pendingResumeFile: string | null = null;
constructor(config: CodexMcpAgentConfig, resumeHandle?: AgentPersistenceHandle) {
if (config.modeId === undefined) {
throw new Error("Codex agent requires modeId to be specified");
}
validateCodexMode(config.modeId);
this.config = config;
this.currentMode =
config.modeId !== undefined ? config.modeId : DEFAULT_CODEX_MODE_ID;
this.currentMode = config.modeId;
this.pendingLocalId = `codex-${randomUUID()}`;
if (resumeHandle) {
@@ -3053,6 +3066,8 @@ class CodexMcpAgentSession implements AgentSession {
}
async setMode(modeId: string): Promise<void> {
validateCodexMode(modeId);
this.currentMode = modeId;
this.config.modeId = modeId;
+1 -2
View File
@@ -243,7 +243,6 @@ export const AgentSnapshotPayloadSchema = z.object({
updatedAt: z.string(),
lastUserMessageAt: z.string().nullable(),
status: AgentStatusSchema,
sessionId: z.string().nullable(),
capabilities: AgentCapabilityFlagsSchema,
currentModeId: z.string().nullable(),
availableModes: z.array(AgentModeSchema),
@@ -340,7 +339,7 @@ export const SendAgentMessageSchema = z.object({
export const SendAgentAudioSchema = z.object({
type: z.literal("send_agent_audio"),
agentId: z.string(),
agentId: z.string().optional(), // Required for auto_run mode, optional for transcribe_only
audio: z.string(), // base64 encoded
format: z.string(),
isLast: z.boolean(),
@@ -49,7 +49,6 @@ function createManagedAgent(
provider,
cwd,
session,
sessionId: overrides.sessionId ?? "session-123",
capabilities:
overrides.capabilities ??
{
+29 -16
View File
@@ -609,7 +609,6 @@ export class Session {
updatedAt: updatedAt.toISOString(),
lastUserMessageAt: lastUserMessageAt ? lastUserMessageAt.toISOString() : null,
status: record.lastStatus,
sessionId: null,
capabilities: defaultCapabilities,
currentModeId: record.lastModeId ?? null,
availableModes: [],
@@ -1107,6 +1106,17 @@ export class Session {
const { agentId, audio, format, isLast, requestId, mode } = msg;
const shouldAutoRun = mode === "auto_run";
// agentId is required for auto_run mode
if (shouldAutoRun && !agentId) {
console.error(
`[Session ${this.clientId}] agentId is required for auto_run mode`
);
return;
}
// Use placeholder for logging when agentId is not provided
const logAgentId = agentId ?? "transcription";
// Decode base64
const audioBuffer = Buffer.from(audio, "base64");
@@ -1114,20 +1124,20 @@ export class Session {
// In the future, we might want to buffer chunks similar to realtime audio
if (!isLast) {
console.log(
`[Session ${this.clientId}] Buffering agent audio chunk for agent ${agentId}`
`[Session ${this.clientId}] Buffering agent audio chunk for agent ${logAgentId}`
);
// TODO: Implement buffering if needed
return;
}
console.log(
`[Session ${this.clientId}] Transcribing audio for agent ${agentId}`
`[Session ${this.clientId}] Transcribing audio for agent ${logAgentId}`
);
try {
// Transcribe the audio
const result = await this.sttManager.transcribe(audioBuffer, format, {
agentId,
agentId: logAgentId,
requestId,
label: shouldAutoRun ? "dictation:auto_run" : "dictation",
});
@@ -1135,13 +1145,13 @@ export class Session {
const transcriptText = result.text.trim();
if (!transcriptText) {
console.log(
`[Session ${this.clientId}] Empty transcription for agent ${agentId}, ignoring`
`[Session ${this.clientId}] Empty transcription for agent ${logAgentId}, ignoring`
);
return;
}
console.log(
`[Session ${this.clientId}] Transcribed audio for agent ${agentId}: ${transcriptText}`
`[Session ${this.clientId}] Transcribed audio for agent ${logAgentId}: ${transcriptText}`
);
// Emit transcription result to client with requestId
@@ -1162,16 +1172,19 @@ export class Session {
if (!shouldAutoRun) {
console.log(
`[Session ${this.clientId}] Completed transcription for agent ${agentId} (requestId: ${requestId ?? "n/a"})`
`[Session ${this.clientId}] Completed transcription for agent ${logAgentId} (requestId: ${requestId ?? "n/a"})`
);
return;
}
// At this point, agentId is guaranteed to be defined (validated at function start)
const validAgentId = agentId!;
try {
await this.ensureAgentLoaded(agentId);
await this.ensureAgentLoaded(validAgentId);
} catch (error) {
this.handleAgentRunError(
agentId,
validAgentId,
error,
"Failed to initialize agent before sending audio prompt"
);
@@ -1179,10 +1192,10 @@ export class Session {
}
try {
await this.interruptAgentIfRunning(agentId);
await this.interruptAgentIfRunning(validAgentId);
} catch (error) {
this.handleAgentRunError(
agentId,
validAgentId,
error,
"Failed to interrupt running agent before sending audio prompt"
);
@@ -1190,22 +1203,22 @@ export class Session {
}
try {
this.agentManager.recordUserMessage(agentId, transcriptText);
this.agentManager.recordUserMessage(validAgentId, transcriptText);
} catch (recordError) {
console.error(
`[Session ${this.clientId}] Failed to record transcribed user message for agent ${agentId}:`,
`[Session ${this.clientId}] Failed to record transcribed user message for agent ${validAgentId}:`,
recordError
);
}
// Send transcribed text to agent
this.startAgentStream(agentId, transcriptText);
this.startAgentStream(validAgentId, transcriptText);
console.log(
`[Session ${this.clientId}] Sent transcribed text to agent ${agentId}`
`[Session ${this.clientId}] Sent transcribed text to agent ${validAgentId}`
);
} catch (error: any) {
console.error(
`[Session ${this.clientId}] Failed to process audio for agent ${agentId}:`,
`[Session ${this.clientId}] Failed to process audio for agent ${logAgentId}:`,
error
);
this.emit({
@@ -14,6 +14,7 @@ import type {
AgentPersistenceHandle,
AgentProvider,
} from "../agent/agent-sdk-types.js";
import { getAgentProviderDefinition } from "../agent/provider-manifest.js";
// ============================================================================
// Configuration
@@ -147,6 +148,12 @@ export class DaemonClient {
// Record the current queue position so we only check NEW messages
const startPosition = this.messageQueue.length;
// Apply default modeId if not provided (mimics frontend behavior)
const modeId =
options.modeId ??
getAgentProviderDefinition(options.provider).defaultModeId ??
undefined;
this.send({
type: "create_agent_request",
requestId,
@@ -155,7 +162,7 @@ export class DaemonClient {
cwd: options.cwd,
title: options.title,
model: options.model,
modeId: options.modeId,
modeId,
mcpServers: options.mcpServers,
extra: options.extra,
},
@@ -209,6 +216,12 @@ export class DaemonClient {
// Record the current queue position so we only check NEW messages
const startPosition = this.messageQueue.length;
// Apply default modeId if not provided (mimics frontend behavior)
const modeId =
options.modeId ??
getAgentProviderDefinition(options.provider).defaultModeId ??
undefined;
this.send({
type: "create_agent_request",
requestId,
@@ -217,7 +230,7 @@ export class DaemonClient {
cwd: options.cwd,
title: options.title,
model: options.model,
modeId: options.modeId,
modeId,
mcpServers: options.mcpServers,
extra: options.extra,
},