/ src / Task.ts
Task.ts
  1  import { randomBytes } from 'crypto'
  2  import type { AppState } from './state/AppState.js'
  3  import type { AgentId } from './types/ids.js'
  4  import { getTaskOutputPath } from './utils/task/diskOutput.js'
  5  
  6  export type TaskType =
  7    | 'local_bash'
  8    | 'local_agent'
  9    | 'remote_agent'
 10    | 'in_process_teammate'
 11    | 'local_workflow'
 12    | 'monitor_mcp'
 13    | 'dream'
 14  
 15  export type TaskStatus =
 16    | 'pending'
 17    | 'running'
 18    | 'completed'
 19    | 'failed'
 20    | 'killed'
 21  
 22  /**
 23   * True when a task is in a terminal state and will not transition further.
 24   * Used to guard against injecting messages into dead teammates, evicting
 25   * finished tasks from AppState, and orphan-cleanup paths.
 26   */
 27  export function isTerminalTaskStatus(status: TaskStatus): boolean {
 28    return status === 'completed' || status === 'failed' || status === 'killed'
 29  }
 30  
 31  export type TaskHandle = {
 32    taskId: string
 33    cleanup?: () => void
 34  }
 35  
 36  export type SetAppState = (f: (prev: AppState) => AppState) => void
 37  
 38  export type TaskContext = {
 39    abortController: AbortController
 40    getAppState: () => AppState
 41    setAppState: SetAppState
 42  }
 43  
 44  // Base fields shared by all task states
 45  export type TaskStateBase = {
 46    id: string
 47    type: TaskType
 48    status: TaskStatus
 49    description: string
 50    toolUseId?: string
 51    startTime: number
 52    endTime?: number
 53    totalPausedMs?: number
 54    outputFile: string
 55    outputOffset: number
 56    notified: boolean
 57  }
 58  
 59  export type LocalShellSpawnInput = {
 60    command: string
 61    description: string
 62    timeout?: number
 63    toolUseId?: string
 64    agentId?: AgentId
 65    /** UI display variant: description-as-label, dialog title, status bar pill. */
 66    kind?: 'bash' | 'monitor'
 67  }
 68  
 69  // What getTaskByType dispatches for: kill. spawn/render were never
 70  // called polymorphically (removed in #22546). All six kill implementations
 71  // use only setAppState — getAppState/abortController were dead weight.
 72  export type Task = {
 73    name: string
 74    type: TaskType
 75    kill(taskId: string, setAppState: SetAppState): Promise<void>
 76  }
 77  
 78  // Task ID prefixes
 79  const TASK_ID_PREFIXES: Record<string, string> = {
 80    local_bash: 'b', // Keep as 'b' for backward compatibility
 81    local_agent: 'a',
 82    remote_agent: 'r',
 83    in_process_teammate: 't',
 84    local_workflow: 'w',
 85    monitor_mcp: 'm',
 86    dream: 'd',
 87  }
 88  
 89  // Get task ID prefix
 90  function getTaskIdPrefix(type: TaskType): string {
 91    return TASK_ID_PREFIXES[type] ?? 'x'
 92  }
 93  
 94  // Case-insensitive-safe alphabet (digits + lowercase) for task IDs.
 95  // 36^8 ≈ 2.8 trillion combinations, sufficient to resist brute-force symlink attacks.
 96  const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
 97  
 98  export function generateTaskId(type: TaskType): string {
 99    const prefix = getTaskIdPrefix(type)
100    const bytes = randomBytes(8)
101    let id = prefix
102    for (let i = 0; i < 8; i++) {
103      id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length]
104    }
105    return id
106  }
107  
108  export function createTaskStateBase(
109    id: string,
110    type: TaskType,
111    description: string,
112    toolUseId?: string,
113  ): TaskStateBase {
114    return {
115      id,
116      type,
117      status: 'pending',
118      description,
119      toolUseId,
120      startTime: Date.now(),
121      outputFile: getTaskOutputPath(id),
122      outputOffset: 0,
123      notified: false,
124    }
125  }