Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
28 kB · 917 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918import { feature } from 'bun:bundle'import { z } from 'zod/v4'import { isReplBridgeActive } from '../../bootstrap/state.js'import { getReplBridgeHandle } from '../../bridge/replBridgeHandle.js'import type { Tool, ToolUseContext } from '../../Tool.js'import { buildTool, type ToolDef } from '../../Tool.js'import { findTeammateTaskByAgentId } from '../../tasks/InProcessTeammateTask/InProcessTeammateTask.js'import { isLocalAgentTask, queuePendingMessage,} from '../../tasks/LocalAgentTask/LocalAgentTask.js'import { isMainSessionTask } from '../../tasks/LocalMainSessionTask.js'import { toAgentId } from '../../types/ids.js'import { generateRequestId } from '../../utils/agentId.js'import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'import { logForDebugging } from '../../utils/debug.js'import { errorMessage } from '../../utils/errors.js'import { truncate } from '../../utils/format.js'import { gracefulShutdown } from '../../utils/gracefulShutdown.js'import { lazySchema } from '../../utils/lazySchema.js'import { parseAddress } from '../../utils/peerAddress.js'import { semanticBoolean } from '../../utils/semanticBoolean.js'import { jsonStringify } from '../../utils/slowOperations.js'import type { BackendType } from '../../utils/swarm/backends/types.js'import { TEAM_LEAD_NAME } from '../../utils/swarm/constants.js'import { readTeamFileAsync } from '../../utils/swarm/teamHelpers.js'import { getAgentId, getAgentName, getTeammateColor, getTeamName, isTeamLead, isTeammate,} from '../../utils/teammate.js'import { createShutdownApprovedMessage, createShutdownRejectedMessage, createShutdownRequestMessage, writeToMailbox,} from '../../utils/teammateMailbox.js'import { resumeAgentBackground } from '../AgentTool/resumeAgent.js'import { SEND_MESSAGE_TOOL_NAME } from './constants.js'import { DESCRIPTION, getPrompt } from './prompt.js'import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
const StructuredMessage = lazySchema(() => z.discriminatedUnion('type', [ z.object({ type: z.literal('shutdown_request'), reason: z.string().optional(), }), z.object({ type: z.literal('shutdown_response'), request_id: z.string(), approve: semanticBoolean(), reason: z.string().optional(), }), z.object({ type: z.literal('plan_approval_response'), request_id: z.string(), approve: semanticBoolean(), feedback: z.string().optional(), }), ]),)
const inputSchema = lazySchema(() => z.object({ to: z .string() .describe( feature('UDS_INBOX') ? 'Recipient: teammate name, "*" for broadcast, "uds:<socket-path>" for a local peer, or "bridge:<session-id>" for a Remote Control peer (use ListPeers to discover)' : 'Recipient: teammate name, or "*" for broadcast to all teammates', ), summary: z .string() .optional() .describe( 'A 5-10 word summary shown as a preview in the UI (required when message is a string)', ), message: z.union([ z.string().describe('Plain text message content'), StructuredMessage(), ]), }),)type InputSchema = ReturnType<typeof inputSchema>
export type Input = z.infer<InputSchema>
export type MessageRouting = { sender: string senderColor?: string target: string targetColor?: string summary?: string content?: string}
export type MessageOutput = { success: boolean message: string routing?: MessageRouting}
export type BroadcastOutput = { success: boolean message: string recipients: string[] routing?: MessageRouting}
export type RequestOutput = { success: boolean message: string request_id: string target: string}
export type ResponseOutput = { success: boolean message: string request_id?: string}
export type SendMessageToolOutput = | MessageOutput | BroadcastOutput | RequestOutput | ResponseOutput
function findTeammateColor( appState: { teamContext?: { teammates: { [id: string]: { color?: string } } } }, name: string,): string | undefined { const teammates = appState.teamContext?.teammates if (!teammates) return undefined for (const teammate of Object.values(teammates)) { if ('name' in teammate && (teammate as { name: string }).name === name) { return teammate.color } } return undefined}
async function handleMessage( recipientName: string, content: string, summary: string | undefined, context: ToolUseContext,): Promise<{ data: MessageOutput }> { const appState = context.getAppState() const teamName = getTeamName(appState.teamContext) const senderName = getAgentName() || (isTeammate() ? 'teammate' : TEAM_LEAD_NAME) const senderColor = getTeammateColor()
await writeToMailbox( recipientName, { from: senderName, text: content, summary, timestamp: new Date().toISOString(), color: senderColor, }, teamName, )
const recipientColor = findTeammateColor(appState, recipientName)
return { data: { success: true, message: `Message sent to ${recipientName}'s inbox`, routing: { sender: senderName, senderColor, target: `@${recipientName}`, targetColor: recipientColor, summary, content, }, }, }}
async function handleBroadcast( content: string, summary: string | undefined, context: ToolUseContext,): Promise<{ data: BroadcastOutput }> { const appState = context.getAppState() const teamName = getTeamName(appState.teamContext)
if (!teamName) { throw new Error( 'Not in a team context. Create a team with Teammate spawnTeam first, or set CLAUDE_CODE_TEAM_NAME.', ) }
const teamFile = await readTeamFileAsync(teamName) if (!teamFile) { throw new Error(`Team "${teamName}" does not exist`) }
const senderName = getAgentName() || (isTeammate() ? 'teammate' : TEAM_LEAD_NAME) if (!senderName) { throw new Error( 'Cannot broadcast: sender name is required. Set CLAUDE_CODE_AGENT_NAME.', ) }
const senderColor = getTeammateColor()
const recipients: string[] = [] for (const member of teamFile.members) { if (member.name.toLowerCase() === senderName.toLowerCase()) { continue } recipients.push(member.name) }
if (recipients.length === 0) { return { data: { success: true, message: 'No teammates to broadcast to (you are the only team member)', recipients: [], }, } }
for (const recipientName of recipients) { await writeToMailbox( recipientName, { from: senderName, text: content, summary, timestamp: new Date().toISOString(), color: senderColor, }, teamName, ) }
return { data: { success: true, message: `Message broadcast to ${recipients.length} teammate(s): ${recipients.join(', ')}`, recipients, routing: { sender: senderName, senderColor, target: '@team', summary, content, }, }, }}
async function handleShutdownRequest( targetName: string, reason: string | undefined, context: ToolUseContext,): Promise<{ data: RequestOutput }> { const appState = context.getAppState() const teamName = getTeamName(appState.teamContext) const senderName = getAgentName() || TEAM_LEAD_NAME const requestId = generateRequestId('shutdown', targetName)
const shutdownMessage = createShutdownRequestMessage({ requestId, from: senderName, reason, })
await writeToMailbox( targetName, { from: senderName, text: jsonStringify(shutdownMessage), timestamp: new Date().toISOString(), color: getTeammateColor(), }, teamName, )
return { data: { success: true, message: `Shutdown request sent to ${targetName}. Request ID: ${requestId}`, request_id: requestId, target: targetName, }, }}
async function handleShutdownApproval( requestId: string, context: ToolUseContext,): Promise<{ data: ResponseOutput }> { const teamName = getTeamName() const agentId = getAgentId() const agentName = getAgentName() || 'teammate'
logForDebugging( `[SendMessageTool] handleShutdownApproval: teamName=${teamName}, agentId=${agentId}, agentName=${agentName}`, )
let ownPaneId: string | undefined let ownBackendType: BackendType | undefined if (teamName) { const teamFile = await readTeamFileAsync(teamName) if (teamFile && agentId) { const selfMember = teamFile.members.find(m => m.agentId === agentId) if (selfMember) { ownPaneId = selfMember.tmuxPaneId ownBackendType = selfMember.backendType } } }
const approvedMessage = createShutdownApprovedMessage({ requestId, from: agentName, paneId: ownPaneId, backendType: ownBackendType, })
await writeToMailbox( TEAM_LEAD_NAME, { from: agentName, text: jsonStringify(approvedMessage), timestamp: new Date().toISOString(), color: getTeammateColor(), }, teamName, )
if (ownBackendType === 'in-process') { logForDebugging( `[SendMessageTool] In-process teammate ${agentName} approving shutdown - signaling abort`, )
if (agentId) { const appState = context.getAppState() const task = findTeammateTaskByAgentId(agentId, appState.tasks) if (task?.abortController) { task.abortController.abort() logForDebugging( `[SendMessageTool] Aborted controller for in-process teammate ${agentName}`, ) } else { logForDebugging( `[SendMessageTool] Warning: Could not find task/abortController for ${agentName}`, ) } } } else { if (agentId) { const appState = context.getAppState() const task = findTeammateTaskByAgentId(agentId, appState.tasks) if (task?.abortController) { logForDebugging( `[SendMessageTool] Fallback: Found in-process task for ${agentName} via AppState, aborting`, ) task.abortController.abort()
return { data: { success: true, message: `Shutdown approved (fallback path). Agent ${agentName} is now exiting.`, request_id: requestId, }, } } }
setImmediate(async () => { await gracefulShutdown(0, 'other') }) }
return { data: { success: true, message: `Shutdown approved. Sent confirmation to team-lead. Agent ${agentName} is now exiting.`, request_id: requestId, }, }}
async function handleShutdownRejection( requestId: string, reason: string,): Promise<{ data: ResponseOutput }> { const teamName = getTeamName() const agentName = getAgentName() || 'teammate'
const rejectedMessage = createShutdownRejectedMessage({ requestId, from: agentName, reason, })
await writeToMailbox( TEAM_LEAD_NAME, { from: agentName, text: jsonStringify(rejectedMessage), timestamp: new Date().toISOString(), color: getTeammateColor(), }, teamName, )
return { data: { success: true, message: `Shutdown rejected. Reason: "${reason}". Continuing to work.`, request_id: requestId, }, }}
async function handlePlanApproval( recipientName: string, requestId: string, context: ToolUseContext,): Promise<{ data: ResponseOutput }> { const appState = context.getAppState() const teamName = appState.teamContext?.teamName
if (!isTeamLead(appState.teamContext)) { throw new Error( 'Only the team lead can approve plans. Teammates cannot approve their own or other plans.', ) }
const leaderMode = appState.toolPermissionContext.mode const modeToInherit = leaderMode === 'plan' ? 'default' : leaderMode
const approvalResponse = { type: 'plan_approval_response', requestId, approved: true, timestamp: new Date().toISOString(), permissionMode: modeToInherit, }
await writeToMailbox( recipientName, { from: TEAM_LEAD_NAME, text: jsonStringify(approvalResponse), timestamp: new Date().toISOString(), }, teamName, )
return { data: { success: true, message: `Plan approved for ${recipientName}. They will receive the approval and can proceed with implementation.`, request_id: requestId, }, }}
async function handlePlanRejection( recipientName: string, requestId: string, feedback: string, context: ToolUseContext,): Promise<{ data: ResponseOutput }> { const appState = context.getAppState() const teamName = appState.teamContext?.teamName
if (!isTeamLead(appState.teamContext)) { throw new Error( 'Only the team lead can reject plans. Teammates cannot reject their own or other plans.', ) }
const rejectionResponse = { type: 'plan_approval_response', requestId, approved: false, feedback, timestamp: new Date().toISOString(), }
await writeToMailbox( recipientName, { from: TEAM_LEAD_NAME, text: jsonStringify(rejectionResponse), timestamp: new Date().toISOString(), }, teamName, )
return { data: { success: true, message: `Plan rejected for ${recipientName} with feedback: "${feedback}"`, request_id: requestId, }, }}
export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> = buildTool({ name: SEND_MESSAGE_TOOL_NAME, searchHint: 'send messages to agent teammates (swarm protocol)', maxResultSizeChars: 100_000,
userFacingName() { return 'SendMessage' },
get inputSchema(): InputSchema { return inputSchema() }, shouldDefer: true,
isEnabled() { return isAgentSwarmsEnabled() },
isReadOnly(input) { return typeof input.message === 'string' },
backfillObservableInput(input) { if ('type' in input) return if (typeof input.to !== 'string') return
if (input.to === '*') { input.type = 'broadcast' if (typeof input.message === 'string') input.content = input.message } else if (typeof input.message === 'string') { input.type = 'message' input.recipient = input.to input.content = input.message } else if (typeof input.message === 'object' && input.message !== null) { const msg = input.message as { type?: string request_id?: string approve?: boolean reason?: string feedback?: string } input.type = msg.type input.recipient = input.to if (msg.request_id !== undefined) input.request_id = msg.request_id if (msg.approve !== undefined) input.approve = msg.approve const content = msg.reason ?? msg.feedback if (content !== undefined) input.content = content } },
toAutoClassifierInput(input) { if (typeof input.message === 'string') { return `to ${input.to}: ${input.message}` } switch (input.message.type) { case 'shutdown_request': return `shutdown_request to ${input.to}` case 'shutdown_response': return `shutdown_response ${input.message.approve ? 'approve' : 'reject'} ${input.message.request_id}` case 'plan_approval_response': return `plan_approval ${input.message.approve ? 'approve' : 'reject'} to ${input.to}` } },
async checkPermissions(input, _context) { if (feature('UDS_INBOX') && parseAddress(input.to).scheme === 'bridge') { return { behavior: 'ask' as const, message: `Send a message to Remote Control session ${input.to}? It arrives as a user prompt on the receiving Claude (possibly another machine) via Anthropic's servers.`, // safetyCheck (not mode) — permissions.ts guards this before both // bypassPermissions (step 1g) and auto-mode's allowlist/classifier. // Cross-machine prompt injection must stay bypass-immune. decisionReason: { type: 'safetyCheck', reason: 'Cross-machine bridge message requires explicit user consent', classifierApprovable: false, }, } } return { behavior: 'allow' as const, updatedInput: input } },
async validateInput(input, _context) { if (input.to.trim().length === 0) { return { result: false, message: 'to must not be empty', errorCode: 9, } } const addr = parseAddress(input.to) if ( (addr.scheme === 'bridge' || addr.scheme === 'uds') && addr.target.trim().length === 0 ) { return { result: false, message: 'address target must not be empty', errorCode: 9, } } if (input.to.includes('@')) { return { result: false, message: 'to must be a bare teammate name or "*" — there is only one team per session', errorCode: 9, } } if (feature('UDS_INBOX') && parseAddress(input.to).scheme === 'bridge') { // Structured-message rejection first — it's the permanent constraint. // Showing "not connected" first would make the user reconnect only to // hit this error on retry. if (typeof input.message !== 'string') { return { result: false, message: 'structured messages cannot be sent cross-session — only plain text', errorCode: 9, } } // postInterClaudeMessage derives from= via getReplBridgeHandle() — // check handle directly for the init-timing window. Also check // isReplBridgeActive() to reject outbound-only (CCR mirror) mode // where the bridge is write-only and peer messaging is unsupported. if (!getReplBridgeHandle() || !isReplBridgeActive()) { return { result: false, message: 'Remote Control is not connected — cannot send to a bridge: target. Reconnect with /remote-control first.', errorCode: 9, } } return { result: true } } if ( feature('UDS_INBOX') && parseAddress(input.to).scheme === 'uds' && typeof input.message === 'string' ) { // UDS cross-session send: summary isn't rendered (UI.tsx returns null // for string messages), so don't require it. Structured messages fall // through to the rejection below. return { result: true } } if (typeof input.message === 'string') { if (!input.summary || input.summary.trim().length === 0) { return { result: false, message: 'summary is required when message is a string', errorCode: 9, } } return { result: true } }
if (input.to === '*') { return { result: false, message: 'structured messages cannot be broadcast (to: "*")', errorCode: 9, } } if (feature('UDS_INBOX') && parseAddress(input.to).scheme !== 'other') { return { result: false, message: 'structured messages cannot be sent cross-session — only plain text', errorCode: 9, } }
if ( input.message.type === 'shutdown_response' && input.to !== TEAM_LEAD_NAME ) { return { result: false, message: `shutdown_response must be sent to "${TEAM_LEAD_NAME}"`, errorCode: 9, } }
if ( input.message.type === 'shutdown_response' && !input.message.approve && (!input.message.reason || input.message.reason.trim().length === 0) ) { return { result: false, message: 'reason is required when rejecting a shutdown request', errorCode: 9, } }
return { result: true } },
async description() { return DESCRIPTION },
async prompt() { return getPrompt() },
mapToolResultToToolResultBlockParam(data, toolUseID) { return { tool_use_id: toolUseID, type: 'tool_result' as const, content: [ { type: 'text' as const, text: jsonStringify(data), }, ], } },
async call(input, context, canUseTool, assistantMessage) { if (feature('UDS_INBOX') && typeof input.message === 'string') { const addr = parseAddress(input.to) if (addr.scheme === 'bridge') { // Re-check handle — checkPermissions blocks on user approval (can be // minutes). validateInput's check is stale if the bridge dropped // during the prompt wait; without this, from="unknown" ships. // Also re-check isReplBridgeActive for outbound-only mode. if (!getReplBridgeHandle() || !isReplBridgeActive()) { return { data: { success: false, message: `Remote Control disconnected before send — cannot deliver to ${input.to}`, }, } } /* eslint-disable @typescript-eslint/no-require-imports */ const { postInterClaudeMessage } = require('../../bridge/peerSessions.js') as typeof import('../../bridge/peerSessions.js') /* eslint-enable @typescript-eslint/no-require-imports */ const result = await postInterClaudeMessage( addr.target, input.message, ) const preview = input.summary || truncate(input.message, 50) return { data: { success: result.ok, message: result.ok ? `“${preview}” → ${input.to}` : `Failed to send to ${input.to}: ${result.error ?? 'unknown'}`, }, } } if (addr.scheme === 'uds') { /* eslint-disable @typescript-eslint/no-require-imports */ const { sendToUdsSocket } = require('../../utils/udsClient.js') as typeof import('../../utils/udsClient.js') /* eslint-enable @typescript-eslint/no-require-imports */ try { await sendToUdsSocket(addr.target, input.message) const preview = input.summary || truncate(input.message, 50) return { data: { success: true, message: `“${preview}” → ${input.to}`, }, } } catch (e) { return { data: { success: false, message: `Failed to send to ${input.to}: ${errorMessage(e)}`, }, } } } }
// Route to in-process subagent by name or raw agentId before falling // through to ambient-team resolution. Stopped agents are auto-resumed. if (typeof input.message === 'string' && input.to !== '*') { const appState = context.getAppState() const registered = appState.agentNameRegistry.get(input.to) const agentId = registered ?? toAgentId(input.to) if (agentId) { const task = appState.tasks[agentId] if (isLocalAgentTask(task) && !isMainSessionTask(task)) { if (task.status === 'running') { queuePendingMessage( agentId, input.message, context.setAppStateForTasks ?? context.setAppState, ) return { data: { success: true, message: `Message queued for delivery to ${input.to} at its next tool round.`, }, } } // task exists but stopped — auto-resume try { const result = await resumeAgentBackground({ agentId, prompt: input.message, toolUseContext: context, canUseTool, invokingRequestId: assistantMessage?.requestId, }) return { data: { success: true, message: `Agent "${input.to}" was stopped (${task.status}); resumed it in the background with your message. You'll be notified when it finishes. Output: ${result.outputFile}`, }, } } catch (e) { return { data: { success: false, message: `Agent "${input.to}" is stopped (${task.status}) and could not be resumed: ${errorMessage(e)}`, }, } } } else { // task evicted from state — try resume from disk transcript. // agentId is either a registered name or a format-matching raw ID // (toAgentId validates the createAgentId format, so teammate names // never reach this block). try { const result = await resumeAgentBackground({ agentId, prompt: input.message, toolUseContext: context, canUseTool, invokingRequestId: assistantMessage?.requestId, }) return { data: { success: true, message: `Agent "${input.to}" had no active task; resumed from transcript in the background with your message. You'll be notified when it finishes. Output: ${result.outputFile}`, }, } } catch (e) { return { data: { success: false, message: `Agent "${input.to}" is registered but has no transcript to resume. It may have been cleaned up. (${errorMessage(e)})`, }, } } } } }
if (typeof input.message === 'string') { if (input.to === '*') { return handleBroadcast(input.message, input.summary, context) } return handleMessage(input.to, input.message, input.summary, context) }
if (input.to === '*') { throw new Error('structured messages cannot be broadcast') }
switch (input.message.type) { case 'shutdown_request': return handleShutdownRequest(input.to, input.message.reason, context) case 'shutdown_response': if (input.message.approve) { return handleShutdownApproval(input.message.request_id, context) } return handleShutdownRejection( input.message.request_id, input.message.reason!, ) case 'plan_approval_response': if (input.message.approve) { return handlePlanApproval( input.to, input.message.request_id, context, ) } return handlePlanRejection( input.to, input.message.request_id, input.message.feedback ?? 'Plan needs revision', context, ) } },
renderToolUseMessage, renderToolResultMessage, } satisfies ToolDef<InputSchema, SendMessageToolOutput>)