Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
33 kB · 1183 lines
TypeScript
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184/** * Teammate Mailbox - File-based messaging system for agent swarms * * Each teammate has an inbox file at .claude/teams/{team_name}/inboxes/{agent_name}.json * Other teammates can write messages to it, and the recipient sees them as attachments. * * Note: Inboxes are keyed by agent name within a team. */
import { mkdir, readFile, writeFile } from 'fs/promises'import { join } from 'path'import { z } from 'zod/v4'import { TEAMMATE_MESSAGE_TAG } from '../constants/xml.js'import { PermissionModeSchema } from '../entrypoints/sdk/coreSchemas.js'import { SEND_MESSAGE_TOOL_NAME } from '../tools/SendMessageTool/constants.js'import type { Message } from '../types/message.js'import { generateRequestId } from './agentId.js'import { count } from './array.js'import { logForDebugging } from './debug.js'import { getTeamsDir } from './envUtils.js'import { getErrnoCode } from './errors.js'import { lazySchema } from './lazySchema.js'import * as lockfile from './lockfile.js'import { logError } from './log.js'import { jsonParse, jsonStringify } from './slowOperations.js'import type { BackendType } from './swarm/backends/types.js'import { TEAM_LEAD_NAME } from './swarm/constants.js'import { sanitizePathComponent } from './tasks.js'import { getAgentName, getTeammateColor, getTeamName } from './teammate.js'
// Lock options: retry with backoff so concurrent callers (multiple Claudes// in a swarm) wait for the lock instead of failing immediately. The sync// lockSync API blocked the event loop; the async API needs explicit retries// to achieve the same serialization semantics.const LOCK_OPTIONS = { retries: { retries: 10, minTimeout: 5, maxTimeout: 100, },}
export type TeammateMessage = { from: string text: string timestamp: string read: boolean color?: string // Sender's assigned color (e.g., 'red', 'blue', 'green') summary?: string // 5-10 word summary shown as preview in the UI}
/** * Get the path to a teammate's inbox file * Structure: ~/.claude/teams/{team_name}/inboxes/{agent_name}.json */export function getInboxPath(agentName: string, teamName?: string): string { const team = teamName || getTeamName() || 'default' const safeTeam = sanitizePathComponent(team) const safeAgentName = sanitizePathComponent(agentName) const inboxDir = join(getTeamsDir(), safeTeam, 'inboxes') const fullPath = join(inboxDir, `${safeAgentName}.json`) logForDebugging( `[TeammateMailbox] getInboxPath: agent=${agentName}, team=${team}, fullPath=${fullPath}`, ) return fullPath}
/** * Ensure the inbox directory exists for a team */async function ensureInboxDir(teamName?: string): Promise<void> { const team = teamName || getTeamName() || 'default' const safeTeam = sanitizePathComponent(team) const inboxDir = join(getTeamsDir(), safeTeam, 'inboxes') await mkdir(inboxDir, { recursive: true }) logForDebugging(`[TeammateMailbox] Ensured inbox directory: ${inboxDir}`)}
/** * Read all messages from a teammate's inbox * @param agentName - The agent name (not UUID) to read inbox for * @param teamName - Optional team name (defaults to CLAUDE_CODE_TEAM_NAME env var or 'default') */export async function readMailbox( agentName: string, teamName?: string,): Promise<TeammateMessage[]> { const inboxPath = getInboxPath(agentName, teamName) logForDebugging(`[TeammateMailbox] readMailbox: path=${inboxPath}`)
try { const content = await readFile(inboxPath, 'utf-8') const messages = jsonParse(content) as TeammateMessage[] logForDebugging( `[TeammateMailbox] readMailbox: read ${messages.length} message(s)`, ) return messages } catch (error) { const code = getErrnoCode(error) if (code === 'ENOENT') { logForDebugging(`[TeammateMailbox] readMailbox: file does not exist`) return [] } logForDebugging(`Failed to read inbox for ${agentName}: ${error}`) logError(error) return [] }}
/** * Read only unread messages from a teammate's inbox * @param agentName - The agent name (not UUID) to read inbox for * @param teamName - Optional team name */export async function readUnreadMessages( agentName: string, teamName?: string,): Promise<TeammateMessage[]> { const messages = await readMailbox(agentName, teamName) const unread = messages.filter(m => !m.read) logForDebugging( `[TeammateMailbox] readUnreadMessages: ${unread.length} unread of ${messages.length} total`, ) return unread}
/** * Write a message to a teammate's inbox * Uses file locking to prevent race conditions when multiple agents write concurrently * @param recipientName - The recipient's agent name (not UUID) * @param message - The message to write * @param teamName - Optional team name */export async function writeToMailbox( recipientName: string, message: Omit<TeammateMessage, 'read'>, teamName?: string,): Promise<void> { await ensureInboxDir(teamName)
const inboxPath = getInboxPath(recipientName, teamName) const lockFilePath = `${inboxPath}.lock`
logForDebugging( `[TeammateMailbox] writeToMailbox: recipient=${recipientName}, from=${message.from}, path=${inboxPath}`, )
// Ensure the inbox file exists before locking (proper-lockfile requires the file to exist) try { await writeFile(inboxPath, '[]', { encoding: 'utf-8', flag: 'wx' }) logForDebugging(`[TeammateMailbox] writeToMailbox: created new inbox file`) } catch (error) { const code = getErrnoCode(error) if (code !== 'EEXIST') { logForDebugging( `[TeammateMailbox] writeToMailbox: failed to create inbox file: ${error}`, ) logError(error) return } }
let release: (() => Promise<void>) | undefined try { release = await lockfile.lock(inboxPath, { lockfilePath: lockFilePath, ...LOCK_OPTIONS, })
// Re-read messages after acquiring lock to get the latest state const messages = await readMailbox(recipientName, teamName)
const newMessage: TeammateMessage = { ...message, read: false, }
messages.push(newMessage)
await writeFile(inboxPath, jsonStringify(messages, null, 2), 'utf-8') logForDebugging( `[TeammateMailbox] Wrote message to ${recipientName}'s inbox from ${message.from}`, ) } catch (error) { logForDebugging(`Failed to write to inbox for ${recipientName}: ${error}`) logError(error) } finally { if (release) { await release() } }}
/** * Mark a specific message in a teammate's inbox as read by index * Uses file locking to prevent race conditions * @param agentName - The agent name to mark message as read for * @param teamName - Optional team name * @param messageIndex - Index of the message to mark as read */export async function markMessageAsReadByIndex( agentName: string, teamName: string | undefined, messageIndex: number,): Promise<void> { const inboxPath = getInboxPath(agentName, teamName) logForDebugging( `[TeammateMailbox] markMessageAsReadByIndex called: agentName=${agentName}, teamName=${teamName}, index=${messageIndex}, path=${inboxPath}`, )
const lockFilePath = `${inboxPath}.lock`
let release: (() => Promise<void>) | undefined try { logForDebugging( `[TeammateMailbox] markMessageAsReadByIndex: acquiring lock...`, ) release = await lockfile.lock(inboxPath, { lockfilePath: lockFilePath, ...LOCK_OPTIONS, }) logForDebugging(`[TeammateMailbox] markMessageAsReadByIndex: lock acquired`)
// Re-read messages after acquiring lock to get the latest state const messages = await readMailbox(agentName, teamName) logForDebugging( `[TeammateMailbox] markMessageAsReadByIndex: read ${messages.length} messages after lock`, )
if (messageIndex < 0 || messageIndex >= messages.length) { logForDebugging( `[TeammateMailbox] markMessageAsReadByIndex: index ${messageIndex} out of bounds (${messages.length} messages)`, ) return }
const message = messages[messageIndex] if (!message || message.read) { logForDebugging( `[TeammateMailbox] markMessageAsReadByIndex: message already read or missing`, ) return }
messages[messageIndex] = { ...message, read: true }
await writeFile(inboxPath, jsonStringify(messages, null, 2), 'utf-8') logForDebugging( `[TeammateMailbox] markMessageAsReadByIndex: marked message at index ${messageIndex} as read`, ) } catch (error) { const code = getErrnoCode(error) if (code === 'ENOENT') { logForDebugging( `[TeammateMailbox] markMessageAsReadByIndex: file does not exist at ${inboxPath}`, ) return } logForDebugging( `[TeammateMailbox] markMessageAsReadByIndex FAILED for ${agentName}: ${error}`, ) logError(error) } finally { if (release) { await release() logForDebugging( `[TeammateMailbox] markMessageAsReadByIndex: lock released`, ) } }}
/** * Mark all messages in a teammate's inbox as read * Uses file locking to prevent race conditions * @param agentName - The agent name to mark messages as read for * @param teamName - Optional team name */export async function markMessagesAsRead( agentName: string, teamName?: string,): Promise<void> { const inboxPath = getInboxPath(agentName, teamName) logForDebugging( `[TeammateMailbox] markMessagesAsRead called: agentName=${agentName}, teamName=${teamName}, path=${inboxPath}`, )
const lockFilePath = `${inboxPath}.lock`
let release: (() => Promise<void>) | undefined try { logForDebugging(`[TeammateMailbox] markMessagesAsRead: acquiring lock...`) release = await lockfile.lock(inboxPath, { lockfilePath: lockFilePath, ...LOCK_OPTIONS, }) logForDebugging(`[TeammateMailbox] markMessagesAsRead: lock acquired`)
// Re-read messages after acquiring lock to get the latest state const messages = await readMailbox(agentName, teamName) logForDebugging( `[TeammateMailbox] markMessagesAsRead: read ${messages.length} messages after lock`, )
if (messages.length === 0) { logForDebugging( `[TeammateMailbox] markMessagesAsRead: no messages to mark`, ) return }
const unreadCount = count(messages, m => !m.read) logForDebugging( `[TeammateMailbox] markMessagesAsRead: ${unreadCount} unread of ${messages.length} total`, )
// messages comes from jsonParse — fresh, unshared objects safe to mutate for (const m of messages) m.read = true
await writeFile(inboxPath, jsonStringify(messages, null, 2), 'utf-8') logForDebugging( `[TeammateMailbox] markMessagesAsRead: WROTE ${unreadCount} message(s) as read to ${inboxPath}`, ) } catch (error) { const code = getErrnoCode(error) if (code === 'ENOENT') { logForDebugging( `[TeammateMailbox] markMessagesAsRead: file does not exist at ${inboxPath}`, ) return } logForDebugging( `[TeammateMailbox] markMessagesAsRead FAILED for ${agentName}: ${error}`, ) logError(error) } finally { if (release) { await release() logForDebugging(`[TeammateMailbox] markMessagesAsRead: lock released`) } }}
/** * Clear a teammate's inbox (delete all messages) * @param agentName - The agent name to clear inbox for * @param teamName - Optional team name */export async function clearMailbox( agentName: string, teamName?: string,): Promise<void> { const inboxPath = getInboxPath(agentName, teamName)
try { // flag 'r+' throws ENOENT if the file doesn't exist, so we don't // accidentally create an inbox file that wasn't there. await writeFile(inboxPath, '[]', { encoding: 'utf-8', flag: 'r+' }) logForDebugging(`[TeammateMailbox] Cleared inbox for ${agentName}`) } catch (error) { const code = getErrnoCode(error) if (code === 'ENOENT') { return } logForDebugging(`Failed to clear inbox for ${agentName}: ${error}`) logError(error) }}
/** * Format teammate messages as XML for attachment display */export function formatTeammateMessages( messages: Array<{ from: string text: string timestamp: string color?: string summary?: string }>,): string { return messages .map(m => { const colorAttr = m.color ? ` color="${m.color}"` : '' const summaryAttr = m.summary ? ` summary="${m.summary}"` : '' return `<${TEAMMATE_MESSAGE_TAG} teammate_id="${m.from}"${colorAttr}${summaryAttr}>\n${m.text}\n</${TEAMMATE_MESSAGE_TAG}>` }) .join('\n\n')}
/** * Structured message sent when a teammate becomes idle (via Stop hook) */export type IdleNotificationMessage = { type: 'idle_notification' from: string timestamp: string /** Why the agent went idle */ idleReason?: 'available' | 'interrupted' | 'failed' /** Brief summary of the last DM sent this turn (if any) */ summary?: string completedTaskId?: string completedStatus?: 'resolved' | 'blocked' | 'failed' failureReason?: string}
/** * Creates an idle notification message to send to the team leader */export function createIdleNotification( agentId: string, options?: { idleReason?: IdleNotificationMessage['idleReason'] summary?: string completedTaskId?: string completedStatus?: 'resolved' | 'blocked' | 'failed' failureReason?: string },): IdleNotificationMessage { return { type: 'idle_notification', from: agentId, timestamp: new Date().toISOString(), idleReason: options?.idleReason, summary: options?.summary, completedTaskId: options?.completedTaskId, completedStatus: options?.completedStatus, failureReason: options?.failureReason, }}
/** * Checks if a message text contains an idle notification */export function isIdleNotification( messageText: string,): IdleNotificationMessage | null { try { const parsed = jsonParse(messageText) if (parsed && parsed.type === 'idle_notification') { return parsed as IdleNotificationMessage } } catch { // Not JSON or not a valid idle notification } return null}
/** * Permission request message sent from worker to leader via mailbox. * Field names align with SDK `can_use_tool` (snake_case). */export type PermissionRequestMessage = { type: 'permission_request' request_id: string agent_id: string tool_name: string tool_use_id: string description: string input: Record<string, unknown> permission_suggestions: unknown[]}
/** * Permission response message sent from leader to worker via mailbox. * Shape mirrors SDK ControlResponseSchema / ControlErrorResponseSchema. */export type PermissionResponseMessage = | { type: 'permission_response' request_id: string subtype: 'success' response?: { updated_input?: Record<string, unknown> permission_updates?: unknown[] } } | { type: 'permission_response' request_id: string subtype: 'error' error: string }
/** * Creates a permission request message to send to the team leader */export function createPermissionRequestMessage(params: { request_id: string agent_id: string tool_name: string tool_use_id: string description: string input: Record<string, unknown> permission_suggestions?: unknown[]}): PermissionRequestMessage { return { type: 'permission_request', request_id: params.request_id, agent_id: params.agent_id, tool_name: params.tool_name, tool_use_id: params.tool_use_id, description: params.description, input: params.input, permission_suggestions: params.permission_suggestions || [], }}
/** * Creates a permission response message to send back to a worker */export function createPermissionResponseMessage(params: { request_id: string subtype: 'success' | 'error' error?: string updated_input?: Record<string, unknown> permission_updates?: unknown[]}): PermissionResponseMessage { if (params.subtype === 'error') { return { type: 'permission_response', request_id: params.request_id, subtype: 'error', error: params.error || 'Permission denied', } } return { type: 'permission_response', request_id: params.request_id, subtype: 'success', response: { updated_input: params.updated_input, permission_updates: params.permission_updates, }, }}
/** * Checks if a message text contains a permission request */export function isPermissionRequest( messageText: string,): PermissionRequestMessage | null { try { const parsed = jsonParse(messageText) if (parsed && parsed.type === 'permission_request') { return parsed as PermissionRequestMessage } } catch { // Not JSON or not a valid permission request } return null}
/** * Checks if a message text contains a permission response */export function isPermissionResponse( messageText: string,): PermissionResponseMessage | null { try { const parsed = jsonParse(messageText) if (parsed && parsed.type === 'permission_response') { return parsed as PermissionResponseMessage } } catch { // Not JSON or not a valid permission response } return null}
/** * Sandbox permission request message sent from worker to leader via mailbox * This is triggered when sandbox runtime detects a network access to a non-allowed host */export type SandboxPermissionRequestMessage = { type: 'sandbox_permission_request' /** Unique identifier for this request */ requestId: string /** Worker's CLAUDE_CODE_AGENT_ID */ workerId: string /** Worker's CLAUDE_CODE_AGENT_NAME */ workerName: string /** Worker's CLAUDE_CODE_AGENT_COLOR */ workerColor?: string /** The host pattern requesting network access */ hostPattern: { host: string } /** Timestamp when request was created */ createdAt: number}
/** * Sandbox permission response message sent from leader to worker via mailbox */export type SandboxPermissionResponseMessage = { type: 'sandbox_permission_response' /** ID of the request this responds to */ requestId: string /** The host that was approved/denied */ host: string /** Whether the connection is allowed */ allow: boolean /** Timestamp when response was created */ timestamp: string}
/** * Creates a sandbox permission request message to send to the team leader */export function createSandboxPermissionRequestMessage(params: { requestId: string workerId: string workerName: string workerColor?: string host: string}): SandboxPermissionRequestMessage { return { type: 'sandbox_permission_request', requestId: params.requestId, workerId: params.workerId, workerName: params.workerName, workerColor: params.workerColor, hostPattern: { host: params.host }, createdAt: Date.now(), }}
/** * Creates a sandbox permission response message to send back to a worker */export function createSandboxPermissionResponseMessage(params: { requestId: string host: string allow: boolean}): SandboxPermissionResponseMessage { return { type: 'sandbox_permission_response', requestId: params.requestId, host: params.host, allow: params.allow, timestamp: new Date().toISOString(), }}
/** * Checks if a message text contains a sandbox permission request */export function isSandboxPermissionRequest( messageText: string,): SandboxPermissionRequestMessage | null { try { const parsed = jsonParse(messageText) if (parsed && parsed.type === 'sandbox_permission_request') { return parsed as SandboxPermissionRequestMessage } } catch { // Not JSON or not a valid sandbox permission request } return null}
/** * Checks if a message text contains a sandbox permission response */export function isSandboxPermissionResponse( messageText: string,): SandboxPermissionResponseMessage | null { try { const parsed = jsonParse(messageText) if (parsed && parsed.type === 'sandbox_permission_response') { return parsed as SandboxPermissionResponseMessage } } catch { // Not JSON or not a valid sandbox permission response } return null}
/** * Message sent when a teammate requests plan approval from the team leader */export const PlanApprovalRequestMessageSchema = lazySchema(() => z.object({ type: z.literal('plan_approval_request'), from: z.string(), timestamp: z.string(), planFilePath: z.string(), planContent: z.string(), requestId: z.string(), }),)
export type PlanApprovalRequestMessage = z.infer< ReturnType<typeof PlanApprovalRequestMessageSchema>>
/** * Message sent by the team leader in response to a plan approval request */export const PlanApprovalResponseMessageSchema = lazySchema(() => z.object({ type: z.literal('plan_approval_response'), requestId: z.string(), approved: z.boolean(), feedback: z.string().optional(), timestamp: z.string(), permissionMode: PermissionModeSchema().optional(), }),)
export type PlanApprovalResponseMessage = z.infer< ReturnType<typeof PlanApprovalResponseMessageSchema>>
/** * Shutdown request message sent from leader to teammate via mailbox */export const ShutdownRequestMessageSchema = lazySchema(() => z.object({ type: z.literal('shutdown_request'), requestId: z.string(), from: z.string(), reason: z.string().optional(), timestamp: z.string(), }),)
export type ShutdownRequestMessage = z.infer< ReturnType<typeof ShutdownRequestMessageSchema>>
/** * Shutdown approved message sent from teammate to leader via mailbox */export const ShutdownApprovedMessageSchema = lazySchema(() => z.object({ type: z.literal('shutdown_approved'), requestId: z.string(), from: z.string(), timestamp: z.string(), paneId: z.string().optional(), backendType: z.string().optional(), }),)
export type ShutdownApprovedMessage = z.infer< ReturnType<typeof ShutdownApprovedMessageSchema>>
/** * Shutdown rejected message sent from teammate to leader via mailbox */export const ShutdownRejectedMessageSchema = lazySchema(() => z.object({ type: z.literal('shutdown_rejected'), requestId: z.string(), from: z.string(), reason: z.string(), timestamp: z.string(), }),)
export type ShutdownRejectedMessage = z.infer< ReturnType<typeof ShutdownRejectedMessageSchema>>
/** * Creates a shutdown request message to send to a teammate */export function createShutdownRequestMessage(params: { requestId: string from: string reason?: string}): ShutdownRequestMessage { return { type: 'shutdown_request', requestId: params.requestId, from: params.from, reason: params.reason, timestamp: new Date().toISOString(), }}
/** * Creates a shutdown approved message to send to the team leader */export function createShutdownApprovedMessage(params: { requestId: string from: string paneId?: string backendType?: BackendType}): ShutdownApprovedMessage { return { type: 'shutdown_approved', requestId: params.requestId, from: params.from, timestamp: new Date().toISOString(), paneId: params.paneId, backendType: params.backendType, }}
/** * Creates a shutdown rejected message to send to the team leader */export function createShutdownRejectedMessage(params: { requestId: string from: string reason: string}): ShutdownRejectedMessage { return { type: 'shutdown_rejected', requestId: params.requestId, from: params.from, reason: params.reason, timestamp: new Date().toISOString(), }}
/** * Sends a shutdown request to a teammate's mailbox. * This is the core logic extracted for reuse by both the tool and UI components. * * @param targetName - Name of the teammate to send shutdown request to * @param teamName - Optional team name (defaults to CLAUDE_CODE_TEAM_NAME env var) * @param reason - Optional reason for the shutdown request * @returns The request ID and target name */export async function sendShutdownRequestToMailbox( targetName: string, teamName?: string, reason?: string,): Promise<{ requestId: string; target: string }> { const resolvedTeamName = teamName || getTeamName()
// Get sender name (supports in-process teammates via AsyncLocalStorage) const senderName = getAgentName() || TEAM_LEAD_NAME
// Generate a deterministic request ID for this shutdown request const requestId = generateRequestId('shutdown', targetName)
// Create and send the shutdown request message const shutdownMessage = createShutdownRequestMessage({ requestId, from: senderName, reason, })
await writeToMailbox( targetName, { from: senderName, text: jsonStringify(shutdownMessage), timestamp: new Date().toISOString(), color: getTeammateColor(), }, resolvedTeamName, )
return { requestId, target: targetName }}
/** * Checks if a message text contains a shutdown request */export function isShutdownRequest( messageText: string,): ShutdownRequestMessage | null { try { const result = ShutdownRequestMessageSchema().safeParse( jsonParse(messageText), ) if (result.success) return result.data } catch { // Not JSON } return null}
/** * Checks if a message text contains a plan approval request */export function isPlanApprovalRequest( messageText: string,): PlanApprovalRequestMessage | null { try { const result = PlanApprovalRequestMessageSchema().safeParse( jsonParse(messageText), ) if (result.success) return result.data } catch { // Not JSON } return null}
/** * Checks if a message text contains a shutdown approved message */export function isShutdownApproved( messageText: string,): ShutdownApprovedMessage | null { try { const result = ShutdownApprovedMessageSchema().safeParse( jsonParse(messageText), ) if (result.success) return result.data } catch { // Not JSON } return null}
/** * Checks if a message text contains a shutdown rejected message */export function isShutdownRejected( messageText: string,): ShutdownRejectedMessage | null { try { const result = ShutdownRejectedMessageSchema().safeParse( jsonParse(messageText), ) if (result.success) return result.data } catch { // Not JSON } return null}
/** * Checks if a message text contains a plan approval response */export function isPlanApprovalResponse( messageText: string,): PlanApprovalResponseMessage | null { try { const result = PlanApprovalResponseMessageSchema().safeParse( jsonParse(messageText), ) if (result.success) return result.data } catch { // Not JSON } return null}
/** * Task assignment message sent when a task is assigned to a teammate */export type TaskAssignmentMessage = { type: 'task_assignment' taskId: string subject: string description: string assignedBy: string timestamp: string}
/** * Checks if a message text contains a task assignment */export function isTaskAssignment( messageText: string,): TaskAssignmentMessage | null { try { const parsed = jsonParse(messageText) if (parsed && parsed.type === 'task_assignment') { return parsed as TaskAssignmentMessage } } catch { // Not JSON or not a valid task assignment } return null}
/** * Team permission update message sent from leader to teammates via mailbox * Broadcasts a permission update that applies to all teammates */export type TeamPermissionUpdateMessage = { type: 'team_permission_update' /** The permission update to apply */ permissionUpdate: { type: 'addRules' rules: Array<{ toolName: string; ruleContent?: string }> behavior: 'allow' | 'deny' | 'ask' destination: 'session' } /** The directory path that was allowed */ directoryPath: string /** The tool name this applies to */ toolName: string}
/** * Checks if a message text contains a team permission update */export function isTeamPermissionUpdate( messageText: string,): TeamPermissionUpdateMessage | null { try { const parsed = jsonParse(messageText) if (parsed && parsed.type === 'team_permission_update') { return parsed as TeamPermissionUpdateMessage } } catch { // Not JSON or not a valid team permission update } return null}
/** * Mode set request message sent from leader to teammate via mailbox * Uses SDK PermissionModeSchema for validated mode values */export const ModeSetRequestMessageSchema = lazySchema(() => z.object({ type: z.literal('mode_set_request'), mode: PermissionModeSchema(), from: z.string(), }),)
export type ModeSetRequestMessage = z.infer< ReturnType<typeof ModeSetRequestMessageSchema>>
/** * Creates a mode set request message to send to a teammate */export function createModeSetRequestMessage(params: { mode: string from: string}): ModeSetRequestMessage { return { type: 'mode_set_request', mode: params.mode as ModeSetRequestMessage['mode'], from: params.from, }}
/** * Checks if a message text contains a mode set request */export function isModeSetRequest( messageText: string,): ModeSetRequestMessage | null { try { const parsed = ModeSetRequestMessageSchema().safeParse( jsonParse(messageText), ) if (parsed.success) { return parsed.data } } catch { // Not JSON or not a valid mode set request } return null}
/** * Checks if a message text is a structured protocol message that should be * routed by useInboxPoller rather than consumed as raw LLM context. * * These message types have specific handlers in useInboxPoller that route them * to the correct queues (workerPermissions, workerSandboxPermissions, etc.). * If getTeammateMailboxAttachments consumes them first, they get bundled as * raw text in attachments and never reach their intended handlers. */export function isStructuredProtocolMessage(messageText: string): boolean { try { const parsed = jsonParse(messageText) if (!parsed || typeof parsed !== 'object' || !('type' in parsed)) { return false } const type = (parsed as { type: unknown }).type return ( type === 'permission_request' || type === 'permission_response' || type === 'sandbox_permission_request' || type === 'sandbox_permission_response' || type === 'shutdown_request' || type === 'shutdown_approved' || type === 'team_permission_update' || type === 'mode_set_request' || type === 'plan_approval_request' || type === 'plan_approval_response' ) } catch { return false }}
/** * Marks only messages matching a predicate as read, leaving others unread. * Uses the same file-locking mechanism as markMessagesAsRead. */export async function markMessagesAsReadByPredicate( agentName: string, predicate: (msg: TeammateMessage) => boolean, teamName?: string,): Promise<void> { const inboxPath = getInboxPath(agentName, teamName)
const lockFilePath = `${inboxPath}.lock` let release: (() => Promise<void>) | undefined
try { release = await lockfile.lock(inboxPath, { lockfilePath: lockFilePath, ...LOCK_OPTIONS, })
const messages = await readMailbox(agentName, teamName) if (messages.length === 0) { return }
const updatedMessages = messages.map(m => !m.read && predicate(m) ? { ...m, read: true } : m, )
await writeFile(inboxPath, jsonStringify(updatedMessages, null, 2), 'utf-8') } catch (error) { const code = getErrnoCode(error) if (code === 'ENOENT') { return } logError(error) } finally { if (release) { try { await release() } catch { // Lock may have already been released } } }}
/** * Extracts a "[to {name}] {summary}" string from the last assistant message * if it ended with a SendMessage tool_use targeting a peer (not the team lead). * Returns undefined when the turn didn't end with a peer DM. */export function getLastPeerDmSummary(messages: Message[]): string | undefined { for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i] if (!msg) continue
// Stop at wake-up boundary: a user prompt (string content), not tool results (array content) if (msg.type === 'user' && typeof msg.message.content === 'string') { break }
if (msg.type !== 'assistant') continue for (const block of msg.message.content) { if ( block.type === 'tool_use' && block.name === SEND_MESSAGE_TOOL_NAME && typeof block.input === 'object' && block.input !== null && 'to' in block.input && typeof block.input.to === 'string' && block.input.to !== '*' && block.input.to.toLowerCase() !== TEAM_LEAD_NAME.toLowerCase() && 'message' in block.input && typeof block.input.message === 'string' ) { const to = block.input.to const summary = 'summary' in block.input && typeof block.input.summary === 'string' ? block.input.summary : block.input.message.slice(0, 80) return `[to ${to}] ${summary}` } } } return undefined}