source dump of claude code
at main 157 lines 5.0 kB view raw
1// Background task entry for auto-dream (memory consolidation subagent). 2// Makes the otherwise-invisible forked agent visible in the footer pill and 3// Shift+Down dialog. The dream agent itself is unchanged — this is pure UI 4// surfacing via the existing task registry. 5 6import { rollbackConsolidationLock } from '../../services/autoDream/consolidationLock.js' 7import type { SetAppState, Task, TaskStateBase } from '../../Task.js' 8import { createTaskStateBase, generateTaskId } from '../../Task.js' 9import { registerTask, updateTaskState } from '../../utils/task/framework.js' 10 11// Keep only the N most recent turns for live display. 12const MAX_TURNS = 30 13 14// A single assistant turn from the dream agent, tool uses collapsed to a count. 15export type DreamTurn = { 16 text: string 17 toolUseCount: number 18} 19 20// No phase detection — the dream prompt has a 4-stage structure 21// (orient/gather/consolidate/prune) but we don't parse it. Just flip from 22// 'starting' to 'updating' when the first Edit/Write tool_use lands. 23export type DreamPhase = 'starting' | 'updating' 24 25export type DreamTaskState = TaskStateBase & { 26 type: 'dream' 27 phase: DreamPhase 28 sessionsReviewing: number 29 /** 30 * Paths observed in Edit/Write tool_use blocks via onMessage. This is an 31 * INCOMPLETE reflection of what the dream agent actually changed — it misses 32 * any bash-mediated writes and only captures the tool calls we pattern-match. 33 * Treat as "at least these were touched", not "only these were touched". 34 */ 35 filesTouched: string[] 36 /** Assistant text responses, tool uses collapsed. Prompt is NOT included. */ 37 turns: DreamTurn[] 38 abortController?: AbortController 39 /** Stashed so kill can rewind the lock mtime (same path as fork-failure). */ 40 priorMtime: number 41} 42 43export function isDreamTask(task: unknown): task is DreamTaskState { 44 return ( 45 typeof task === 'object' && 46 task !== null && 47 'type' in task && 48 task.type === 'dream' 49 ) 50} 51 52export function registerDreamTask( 53 setAppState: SetAppState, 54 opts: { 55 sessionsReviewing: number 56 priorMtime: number 57 abortController: AbortController 58 }, 59): string { 60 const id = generateTaskId('dream') 61 const task: DreamTaskState = { 62 ...createTaskStateBase(id, 'dream', 'dreaming'), 63 type: 'dream', 64 status: 'running', 65 phase: 'starting', 66 sessionsReviewing: opts.sessionsReviewing, 67 filesTouched: [], 68 turns: [], 69 abortController: opts.abortController, 70 priorMtime: opts.priorMtime, 71 } 72 registerTask(task, setAppState) 73 return id 74} 75 76export function addDreamTurn( 77 taskId: string, 78 turn: DreamTurn, 79 touchedPaths: string[], 80 setAppState: SetAppState, 81): void { 82 updateTaskState<DreamTaskState>(taskId, setAppState, task => { 83 const seen = new Set(task.filesTouched) 84 const newTouched = touchedPaths.filter(p => !seen.has(p) && seen.add(p)) 85 // Skip the update entirely if the turn is empty AND nothing new was 86 // touched. Avoids re-rendering on pure no-ops. 87 if ( 88 turn.text === '' && 89 turn.toolUseCount === 0 && 90 newTouched.length === 0 91 ) { 92 return task 93 } 94 return { 95 ...task, 96 phase: newTouched.length > 0 ? 'updating' : task.phase, 97 filesTouched: 98 newTouched.length > 0 99 ? [...task.filesTouched, ...newTouched] 100 : task.filesTouched, 101 turns: task.turns.slice(-(MAX_TURNS - 1)).concat(turn), 102 } 103 }) 104} 105 106export function completeDreamTask( 107 taskId: string, 108 setAppState: SetAppState, 109): void { 110 // notified: true immediately — dream has no model-facing notification path 111 // (it's UI-only), and eviction requires terminal + notified. The inline 112 // appendSystemMessage completion note IS the user surface. 113 updateTaskState<DreamTaskState>(taskId, setAppState, task => ({ 114 ...task, 115 status: 'completed', 116 endTime: Date.now(), 117 notified: true, 118 abortController: undefined, 119 })) 120} 121 122export function failDreamTask(taskId: string, setAppState: SetAppState): void { 123 updateTaskState<DreamTaskState>(taskId, setAppState, task => ({ 124 ...task, 125 status: 'failed', 126 endTime: Date.now(), 127 notified: true, 128 abortController: undefined, 129 })) 130} 131 132export const DreamTask: Task = { 133 name: 'DreamTask', 134 type: 'dream', 135 136 async kill(taskId, setAppState) { 137 let priorMtime: number | undefined 138 updateTaskState<DreamTaskState>(taskId, setAppState, task => { 139 if (task.status !== 'running') return task 140 task.abortController?.abort() 141 priorMtime = task.priorMtime 142 return { 143 ...task, 144 status: 'killed', 145 endTime: Date.now(), 146 notified: true, 147 abortController: undefined, 148 } 149 }) 150 // Rewind the lock mtime so the next session can retry. Same path as the 151 // fork-failure catch in autoDream.ts. If updateTaskState was a no-op 152 // (already terminal), priorMtime stays undefined and we skip. 153 if (priorMtime !== undefined) { 154 await rollbackConsolidationLock(priorMtime) 155 } 156 }, 157}