Files
claude-code/src/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx
unraid bddffa216a feat: integrate 5 feature branches, upstream fixes, and MIME detection fix
Squashed 5 commits:

Features (from 5 feature branches):
- MCP fix, pipe mute, stub recovery
- KAIROS activation, openclaw autonomy
- Daemon/job command hierarchy + cross-platform bg engine

Upstream fixes:
- fix: Bun.hash compatibility
- chore: chrome dependency update
- docs: browser support guide

MIME detection fix:
- Screenshot detectMimeFromBase64(): decode raw bytes from base64
  instead of broken charCodeAt comparison
- Fixes API 400 on Windows (JPEG) and macOS (PNG) screenshots
2026-04-14 18:32:19 +08:00

176 lines
5.1 KiB
TypeScript

/**
* InProcessTeammateTask - Manages in-process teammate lifecycle
*
* This component implements the Task interface for in-process teammates.
* Unlike LocalAgentTask (background agents), in-process teammates:
* 1. Run in the same Node.js process using AsyncLocalStorage for isolation
* 2. Have team-aware identity (agentName@teamName)
* 3. Support plan mode approval flow
* 4. Can be idle (waiting for work) or active (processing)
*/
import {
isTerminalTaskStatus,
type SetAppState,
type Task,
type TaskStateBase,
} from '../../Task.js'
import type { Message } from '../../types/message.js'
import { logForDebugging } from '../../utils/debug.js'
import { createUserMessage } from '../../utils/messages.js'
import { killInProcessTeammate } from '../../utils/swarm/spawnInProcess.js'
import { updateTaskState } from '../../utils/task/framework.js'
import type { InProcessTeammateTaskState } from './types.js'
import { appendCappedMessage, isInProcessTeammateTask } from './types.js'
/**
* InProcessTeammateTask - Handles in-process teammate execution.
*/
export const InProcessTeammateTask: Task = {
name: 'InProcessTeammateTask',
type: 'in_process_teammate',
async kill(taskId, setAppState) {
killInProcessTeammate(taskId, setAppState)
},
}
/**
* Request shutdown for a teammate.
*/
export function requestTeammateShutdown(
taskId: string,
setAppState: SetAppState,
): void {
updateTaskState<InProcessTeammateTaskState>(taskId, setAppState, task => {
if (task.status !== 'running' || task.shutdownRequested) {
return task
}
return {
...task,
shutdownRequested: true,
}
})
}
/**
* Append a message to a teammate's conversation history.
* Used for zoomed view to show the teammate's conversation.
*/
export function appendTeammateMessage(
taskId: string,
message: Message,
setAppState: SetAppState,
): void {
updateTaskState<InProcessTeammateTaskState>(taskId, setAppState, task => {
if (task.status !== 'running') {
return task
}
return {
...task,
messages: appendCappedMessage(task.messages, message),
}
})
}
/**
* Inject a user message to a teammate's pending queue.
* Used when viewing a teammate's transcript to send typed messages to them.
* Also adds the message to task.messages so it appears immediately in the transcript.
*/
export function injectUserMessageToTeammate(
taskId: string,
message: string,
options: {
autonomyRunId?: string
origin?: Message['origin']
} | undefined,
setAppState: SetAppState,
): boolean {
let injected = false
updateTaskState<InProcessTeammateTaskState>(taskId, setAppState, task => {
// Allow message injection when teammate is running or idle (waiting for input)
// Only reject if teammate is in a terminal state
if (isTerminalTaskStatus(task.status)) {
logForDebugging(
`Dropping message for teammate task ${taskId}: task status is "${task.status}"`,
)
return task
}
injected = true
return {
...task,
pendingUserMessages: [
...task.pendingUserMessages,
{
message,
...(options?.autonomyRunId
? { autonomyRunId: options.autonomyRunId }
: {}),
...(options?.origin ? { origin: options.origin } : {}),
},
],
messages: appendCappedMessage(
task.messages,
createUserMessage({
content: message,
...(options?.origin ? { origin: options.origin } : {}),
}),
),
}
})
return injected
}
/**
* Get teammate task by agent ID from AppState.
* Prefers running tasks over killed/completed ones in case multiple tasks
* with the same agentId exist.
* Returns undefined if not found.
*/
export function findTeammateTaskByAgentId(
agentId: string,
tasks: Record<string, TaskStateBase>,
): InProcessTeammateTaskState | undefined {
let fallback: InProcessTeammateTaskState | undefined
for (const task of Object.values(tasks)) {
if (isInProcessTeammateTask(task) && task.identity.agentId === agentId) {
// Prefer running tasks in case old killed tasks still exist in AppState
// alongside new running ones with the same agentId
if (task.status === 'running') {
return task
}
// Keep first match as fallback in case no running task exists
if (!fallback) {
fallback = task
}
}
}
return fallback
}
/**
* Get all in-process teammate tasks from AppState.
*/
export function getAllInProcessTeammateTasks(
tasks: Record<string, TaskStateBase>,
): InProcessTeammateTaskState[] {
return Object.values(tasks).filter(isInProcessTeammateTask)
}
/**
* Get running in-process teammates sorted alphabetically by agentName.
* Shared between TeammateSpinnerTree display, PromptInput footer selector,
* and useBackgroundTaskNavigation — selectedIPAgentIndex maps into this
* array, so all three must agree on sort order.
*/
export function getRunningTeammatesSorted(
tasks: Record<string, TaskStateBase>,
): InProcessTeammateTaskState[] {
return getAllInProcessTeammateTasks(tasks)
.filter(t => t.status === 'running')
.sort((a, b) => a.identity.agentName.localeCompare(b.identity.agentName))
}