source dump of claude code
1import type { TaskStateBase } from '../../Task.js'
2import type { AgentToolResult } from '../../tools/AgentTool/agentToolUtils.js'
3import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js'
4import type { Message } from '../../types/message.js'
5import type { PermissionMode } from '../../utils/permissions/PermissionMode.js'
6import type { AgentProgress } from '../LocalAgentTask/LocalAgentTask.js'
7
8/**
9 * Teammate identity stored in task state.
10 * Same shape as TeammateContext (runtime) but stored as plain data.
11 * TeammateContext is for AsyncLocalStorage; this is for AppState persistence.
12 */
13export type TeammateIdentity = {
14 agentId: string // e.g., "researcher@my-team"
15 agentName: string // e.g., "researcher"
16 teamName: string
17 color?: string
18 planModeRequired: boolean
19 parentSessionId: string // Leader's session ID
20}
21
22export type InProcessTeammateTaskState = TaskStateBase & {
23 type: 'in_process_teammate'
24
25 // Identity as sub-object (matches TeammateContext shape for consistency)
26 // Stored as plain data in AppState, NOT a reference to AsyncLocalStorage
27 identity: TeammateIdentity
28
29 // Execution
30 prompt: string
31 // Optional model override for this teammate
32 model?: string
33 // Optional: Only set if teammate uses a specific agent definition
34 // Many teammates run as general-purpose agents without a predefined definition
35 selectedAgent?: AgentDefinition
36 abortController?: AbortController // Runtime only, not serialized to disk - kills WHOLE teammate
37 currentWorkAbortController?: AbortController // Runtime only - aborts current turn without killing teammate
38 unregisterCleanup?: () => void // Runtime only
39
40 // Plan mode approval tracking (planModeRequired is in identity)
41 awaitingPlanApproval: boolean
42
43 // Permission mode for this teammate (cycled independently via Shift+Tab when viewing)
44 permissionMode: PermissionMode
45
46 // State
47 error?: string
48 result?: AgentToolResult // Reuse existing type since teammates run via runAgent()
49 progress?: AgentProgress
50
51 // Conversation history for zoomed view (NOT mailbox messages)
52 // Mailbox messages are stored separately in teamContext.inProcessMailboxes
53 messages?: Message[]
54
55 // Tool use IDs currently being executed (for animation in transcript view)
56 inProgressToolUseIDs?: Set<string>
57
58 // Queue of user messages to deliver when viewing teammate transcript
59 pendingUserMessages: string[]
60
61 // UI: random spinner verbs (stable across re-renders, shared between components)
62 spinnerVerb?: string
63 pastTenseVerb?: string
64
65 // Lifecycle
66 isIdle: boolean
67 shutdownRequested: boolean
68
69 // Callbacks to notify when teammate becomes idle (runtime only)
70 // Used by leader to efficiently wait without polling
71 onIdleCallbacks?: Array<() => void>
72
73 // Progress tracking (for computing deltas in notifications)
74 lastReportedToolCount: number
75 lastReportedTokenCount: number
76}
77
78export function isInProcessTeammateTask(
79 task: unknown,
80): task is InProcessTeammateTaskState {
81 return (
82 typeof task === 'object' &&
83 task !== null &&
84 'type' in task &&
85 task.type === 'in_process_teammate'
86 )
87}
88
89/**
90 * Cap on the number of messages kept in task.messages (the AppState UI mirror).
91 *
92 * task.messages exists purely for the zoomed transcript dialog, which only
93 * needs recent context. The full conversation lives in the local allMessages
94 * array (inProcessRunner) and on disk at the agent transcript path.
95 *
96 * BQ analysis (round 9, 2026-03-20) showed ~20MB RSS per agent at 500+ turn
97 * sessions and ~125MB per concurrent agent in swarm bursts. Whale session
98 * 9a990de8 launched 292 agents in 2 minutes and reached 36.8GB. The dominant
99 * cost is this array holding a second full copy of every message.
100 */
101export const TEAMMATE_MESSAGES_UI_CAP = 50
102
103/**
104 * Append an item to a message array, capping the result at
105 * TEAMMATE_MESSAGES_UI_CAP entries by dropping the oldest. Always returns
106 * a new array (AppState immutability).
107 */
108export function appendCappedMessage<T>(
109 prev: readonly T[] | undefined,
110 item: T,
111): T[] {
112 if (prev === undefined || prev.length === 0) {
113 return [item]
114 }
115 if (prev.length >= TEAMMATE_MESSAGES_UI_CAP) {
116 const next = prev.slice(-(TEAMMATE_MESSAGES_UI_CAP - 1))
117 next.push(item)
118 return next
119 }
120 return [...prev, item]
121}