| 1831 | } |
| 1832 | |
| 1833 | async initialize(): Promise<void> { |
| 1834 | const startupStartedAt = Date.now(); |
| 1835 | const startupConfig = this.config.loadConfigOrDefault(); |
| 1836 | const queuedTaskCountAtStartup = this.listAgentTaskWorkspaces(startupConfig).filter( |
| 1837 | (task) => task.taskStatus === "queued" && typeof task.id === "string" |
| 1838 | ).length; |
| 1839 | |
| 1840 | log.info("[startup] TaskService.initialize starting", { |
| 1841 | queuedTaskCountAtStartup, |
| 1842 | }); |
| 1843 | |
| 1844 | const staleStartingTasks = this.listAgentTaskWorkspaces(startupConfig).filter( |
| 1845 | (task) => task.taskStatus === "starting" && typeof task.id === "string" |
| 1846 | ); |
| 1847 | if (staleStartingTasks.length > 0) { |
| 1848 | const recoveries = new Map< |
| 1849 | string, |
| 1850 | { status: Extract<AgentTaskStatus, "queued" | "running">; acceptedPrompt: boolean } |
| 1851 | >(); |
| 1852 | for (const task of staleStartingTasks) { |
| 1853 | assert(task.id != null && task.id.length > 0, "stale starting task id is required"); |
| 1854 | const isStreaming = this.aiService.isStreaming(task.id); |
| 1855 | recoveries.set(task.id, { |
| 1856 | status: isStreaming ? "running" : "queued", |
| 1857 | acceptedPrompt: !isStreaming && (await this.hasAcceptedInitialTaskPrompt(task.id)), |
| 1858 | }); |
| 1859 | } |
| 1860 | |
| 1861 | await this.config.editConfig((config) => { |
| 1862 | for (const task of staleStartingTasks) { |
| 1863 | assert(task.id != null && task.id.length > 0, "stale starting task id is required"); |
| 1864 | const recovery = recoveries.get(task.id); |
| 1865 | assert(recovery != null, "stale starting task recovery is required"); |
| 1866 | const entry = findWorkspaceEntry(config, task.id); |
| 1867 | if (!entry) continue; |
| 1868 | entry.workspace.taskStatus = recovery.status; |
| 1869 | if (recovery.acceptedPrompt) { |
| 1870 | // The initial prompt is already durable in chat history; clearing taskPrompt makes the |
| 1871 | // queued recovery path resume that accepted turn instead of appending a duplicate user turn. |
| 1872 | entry.workspace.taskPrompt = undefined; |
| 1873 | } |
| 1874 | } |
| 1875 | return config; |
| 1876 | }); |
| 1877 | log.info("[startup] Recovered stale starting agent tasks", { |
| 1878 | count: staleStartingTasks.length, |
| 1879 | acceptedPromptCount: [...recoveries.values()].filter((recovery) => recovery.acceptedPrompt) |
| 1880 | .length, |
| 1881 | }); |
| 1882 | } |
| 1883 | |
| 1884 | const maybeStartQueuedTasksStartedAt = Date.now(); |
| 1885 | await this.maybeStartQueuedTasks(); |
| 1886 | const maybeStartQueuedTasksMs = Date.now() - maybeStartQueuedTasksStartedAt; |
| 1887 | |
| 1888 | let config = this.config.loadConfigOrDefault(); |
| 1889 | let taskIndex = this.buildAgentTaskIndex(config); |
| 1890 | // Recompute the startup recovery candidate lists from a config snapshot. Hoisted into a |