import { randomBytes } from 'crypto' import type { AppState } from './state/AppState.js' import type { AgentId } from './types/ids.js' import { getTaskOutputPath } from './utils/task/diskOutput.js' export type TaskType = | 'local_bash' | 'local_agent' | 'remote_agent' | 'in_process_teammate' | 'local_workflow' | 'monitor_mcp' | 'dream' export type TaskStatus = | 'pending' | 'running' | 'completed' | 'failed' | 'killed' /** * True when a task is in a terminal state and will not transition further. * Used to guard against injecting messages into dead teammates, evicting * finished tasks from AppState, and orphan-cleanup paths. */ export function isTerminalTaskStatus(status: TaskStatus): boolean { return status === 'completed' || status === 'failed' || status === 'killed' } export type TaskHandle = { taskId: string cleanup?: () => void } export type SetAppState = (f: (prev: AppState) => AppState) => void export type TaskContext = { abortController: AbortController getAppState: () => AppState setAppState: SetAppState } // Base fields shared by all task states export type TaskStateBase = { id: string type: TaskType status: TaskStatus description: string toolUseId?: string startTime: number endTime?: number totalPausedMs?: number outputFile: string outputOffset: number notified: boolean } export type LocalShellSpawnInput = { command: string description: string timeout?: number toolUseId?: string agentId?: AgentId /** UI display variant: description-as-label, dialog title, status bar pill. */ kind?: 'bash' | 'monitor' } // What getTaskByType dispatches for: kill. spawn/render were never // called polymorphically (removed in #22546). All six kill implementations // use only setAppState — getAppState/abortController were dead weight. export type Task = { name: string type: TaskType kill(taskId: string, setAppState: SetAppState): Promise } // Task ID prefixes const TASK_ID_PREFIXES: Record = { local_bash: 'b', // Keep as 'b' for backward compatibility local_agent: 'a', remote_agent: 'r', in_process_teammate: 't', local_workflow: 'w', monitor_mcp: 'm', dream: 'd', } // Get task ID prefix function getTaskIdPrefix(type: TaskType): string { return TASK_ID_PREFIXES[type] ?? 'x' } // Case-insensitive-safe alphabet (digits + lowercase) for task IDs. // 36^8 ≈ 2.8 trillion combinations, sufficient to resist brute-force symlink attacks. const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz' export function generateTaskId(type: TaskType): string { const prefix = getTaskIdPrefix(type) const bytes = randomBytes(8) let id = prefix for (let i = 0; i < 8; i++) { id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length] } return id } export function createTaskStateBase( id: string, type: TaskType, description: string, toolUseId?: string, ): TaskStateBase { return { id, type, status: 'pending', description, toolUseId, startTime: Date.now(), outputFile: getTaskOutputPath(id), outputOffset: 0, notified: false, } }