( setAppState: SetAppState, updatedTaskOffsets: Record<string, number>, evictedTaskIds: string[], )
| 211 | * so concurrent status transitions aren't clobbered. |
| 212 | */ |
| 213 | export function applyTaskOffsetsAndEvictions( |
| 214 | setAppState: SetAppState, |
| 215 | updatedTaskOffsets: Record<string, number>, |
| 216 | evictedTaskIds: string[], |
| 217 | ): void { |
| 218 | const offsetIds = Object.keys(updatedTaskOffsets) |
| 219 | if (offsetIds.length === 0 && evictedTaskIds.length === 0) { |
| 220 | return |
| 221 | } |
| 222 | setAppState(prev => { |
| 223 | let changed = false |
| 224 | const newTasks = { ...prev.tasks } |
| 225 | for (const id of offsetIds) { |
| 226 | const fresh = newTasks[id] |
| 227 | // Re-check status on fresh state — task may have completed during the |
| 228 | // await. If it's no longer running, the offset update is moot. |
| 229 | if (fresh?.status === 'running') { |
| 230 | newTasks[id] = { ...fresh, outputOffset: updatedTaskOffsets[id]! } |
| 231 | changed = true |
| 232 | } |
| 233 | } |
| 234 | for (const id of evictedTaskIds) { |
| 235 | const fresh = newTasks[id] |
| 236 | // Re-check terminal+notified on fresh state (TOCTOU: resume may have |
| 237 | // replaced the task during the generateTaskAttachments await) |
| 238 | if (!fresh || !isTerminalTaskStatus(fresh.status) || !fresh.notified) { |
| 239 | continue |
| 240 | } |
| 241 | if ('retain' in fresh && (fresh.evictAfter ?? Infinity) > Date.now()) { |
| 242 | continue |
| 243 | } |
| 244 | delete newTasks[id] |
| 245 | changed = true |
| 246 | } |
| 247 | return changed ? { ...prev, tasks: newTasks } : prev |
| 248 | }) |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * Poll all running tasks and check for updates. |
no test coverage detected