@swarmclawai/swarmclaw 1.2.4 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/README.md +14 -0
  2. package/bin/daemon-cmd.js +169 -0
  3. package/bin/server-cmd.js +3 -0
  4. package/bin/swarmclaw.js +11 -0
  5. package/package.json +17 -16
  6. package/src/app/api/agents/[id]/clone/route.ts +3 -32
  7. package/src/app/api/agents/[id]/route.ts +6 -158
  8. package/src/app/api/agents/[id]/status/route.ts +2 -3
  9. package/src/app/api/agents/[id]/thread/route.ts +4 -17
  10. package/src/app/api/agents/bulk/route.ts +5 -47
  11. package/src/app/api/agents/route.ts +5 -119
  12. package/src/app/api/agents/trash/route.ts +13 -24
  13. package/src/app/api/auth/route.ts +3 -9
  14. package/src/app/api/autonomy/estop/route.ts +5 -5
  15. package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
  16. package/src/app/api/chatrooms/[id]/route.ts +23 -2
  17. package/src/app/api/chatrooms/route.ts +13 -2
  18. package/src/app/api/chats/[id]/clear/route.ts +2 -13
  19. package/src/app/api/chats/[id]/deploy/route.ts +2 -3
  20. package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
  21. package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
  22. package/src/app/api/chats/[id]/queue/route.ts +17 -64
  23. package/src/app/api/chats/[id]/retry/route.ts +4 -22
  24. package/src/app/api/chats/[id]/route.ts +10 -138
  25. package/src/app/api/chats/heartbeat/route.ts +2 -1
  26. package/src/app/api/chats/migrate-messages/route.ts +7 -0
  27. package/src/app/api/chats/route.ts +13 -134
  28. package/src/app/api/connectors/[id]/access/route.ts +12 -229
  29. package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
  30. package/src/app/api/connectors/[id]/health/route.ts +12 -39
  31. package/src/app/api/connectors/[id]/route.ts +14 -122
  32. package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
  33. package/src/app/api/connectors/doctor/route.ts +1 -1
  34. package/src/app/api/connectors/route.ts +12 -70
  35. package/src/app/api/credentials/[id]/route.ts +2 -4
  36. package/src/app/api/credentials/route.ts +10 -19
  37. package/src/app/api/daemon/health-check/route.ts +3 -4
  38. package/src/app/api/daemon/route.ts +10 -8
  39. package/src/app/api/documents/route.ts +11 -10
  40. package/src/app/api/external-agents/route.ts +3 -3
  41. package/src/app/api/gateways/[id]/health/route.ts +2 -3
  42. package/src/app/api/gateways/[id]/route.ts +7 -122
  43. package/src/app/api/gateways/route.ts +3 -103
  44. package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
  45. package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
  46. package/src/app/api/openclaw/directory/route.ts +2 -2
  47. package/src/app/api/openclaw/history/route.ts +3 -5
  48. package/src/app/api/providers/[id]/route.test.ts +49 -0
  49. package/src/app/api/providers/ollama/route.ts +6 -5
  50. package/src/app/api/schedules/[id]/route.ts +14 -108
  51. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  52. package/src/app/api/schedules/route.ts +9 -51
  53. package/src/app/api/settings/route.ts +4 -3
  54. package/src/app/api/setup/check-provider/route.ts +15 -1
  55. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  56. package/src/app/api/system/status/route.ts +2 -2
  57. package/src/app/api/tasks/[id]/route.ts +16 -202
  58. package/src/app/api/tasks/bulk/route.ts +5 -86
  59. package/src/app/api/tasks/metrics/route.ts +2 -1
  60. package/src/app/api/tasks/route.ts +11 -171
  61. package/src/app/api/upload/route.ts +1 -1
  62. package/src/app/api/uploads/[filename]/route.ts +1 -1
  63. package/src/app/api/uploads/route.ts +1 -1
  64. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  65. package/src/app/layout.tsx +9 -6
  66. package/src/app/protocols/page.tsx +71 -89
  67. package/src/app/tasks/page.tsx +32 -32
  68. package/src/cli/index.js +1 -0
  69. package/src/cli/spec.js +1 -0
  70. package/src/components/agents/agent-sheet.tsx +5 -5
  71. package/src/components/auth/setup-wizard/index.tsx +4 -4
  72. package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
  73. package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
  74. package/src/components/auth/setup-wizard/utils.ts +1 -1
  75. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  76. package/src/components/connectors/connector-list.tsx +26 -40
  77. package/src/components/connectors/connector-sheet.tsx +95 -149
  78. package/src/components/gateways/gateway-sheet.tsx +61 -110
  79. package/src/components/layout/live-query-sync.tsx +121 -0
  80. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  81. package/src/components/providers/app-query-provider.tsx +17 -0
  82. package/src/components/providers/provider-list.tsx +60 -61
  83. package/src/components/providers/provider-sheet.tsx +74 -56
  84. package/src/components/skills/skill-list.tsx +5 -18
  85. package/src/components/skills/skill-sheet.tsx +21 -20
  86. package/src/components/skills/skills-workspace.tsx +48 -87
  87. package/src/components/tasks/task-card.tsx +20 -13
  88. package/src/components/tasks/task-column.tsx +22 -7
  89. package/src/components/tasks/task-list.tsx +8 -11
  90. package/src/components/tasks/task-sheet.tsx +111 -103
  91. package/src/features/agents/queries.ts +20 -0
  92. package/src/features/chatrooms/queries.ts +20 -0
  93. package/src/features/chats/queries.ts +27 -0
  94. package/src/features/connectors/queries.ts +145 -0
  95. package/src/features/credentials/queries.ts +37 -0
  96. package/src/features/extensions/queries.ts +26 -0
  97. package/src/features/external-agents/queries.ts +36 -0
  98. package/src/features/gateways/queries.ts +274 -0
  99. package/src/features/missions/queries.ts +23 -0
  100. package/src/features/projects/queries.ts +20 -0
  101. package/src/features/protocols/queries.ts +149 -0
  102. package/src/features/providers/queries.ts +142 -0
  103. package/src/features/settings/queries.ts +20 -0
  104. package/src/features/skills/queries.ts +182 -0
  105. package/src/features/tasks/queries.ts +189 -0
  106. package/src/hooks/use-ws.ts +3 -2
  107. package/src/lib/app/api-client.ts +2 -2
  108. package/src/lib/query/client.ts +17 -0
  109. package/src/lib/server/agents/agent-runtime-config.ts +1 -1
  110. package/src/lib/server/agents/agent-service.ts +429 -0
  111. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  112. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  113. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  114. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  115. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  116. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  117. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  118. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  119. package/src/lib/server/build-llm.ts +7 -15
  120. package/src/lib/server/capability-router.test.ts +70 -1
  121. package/src/lib/server/capability-router.ts +24 -99
  122. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  123. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  124. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  125. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  126. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  127. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  128. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  129. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  130. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  131. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  132. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  133. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  134. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  135. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  136. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  137. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  138. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  139. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  140. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  141. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  142. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  143. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  144. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  145. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  146. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  147. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  148. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  149. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  150. package/src/lib/server/chats/chat-session-service.ts +410 -0
  151. package/src/lib/server/connectors/access.ts +1 -1
  152. package/src/lib/server/connectors/commands.ts +7 -6
  153. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  154. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  155. package/src/lib/server/connectors/connector-service.ts +453 -0
  156. package/src/lib/server/connectors/delivery.ts +17 -12
  157. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  158. package/src/lib/server/connectors/media.ts +1 -1
  159. package/src/lib/server/connectors/response-media.ts +1 -1
  160. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  161. package/src/lib/server/connectors/session.ts +9 -7
  162. package/src/lib/server/connectors/voice-note.ts +2 -1
  163. package/src/lib/server/context-manager.ts +20 -1
  164. package/src/lib/server/cost.ts +2 -3
  165. package/src/lib/server/credentials/credential-repository.ts +43 -4
  166. package/src/lib/server/credentials/credential-service.ts +112 -0
  167. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  168. package/src/lib/server/daemon/controller.ts +577 -0
  169. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  170. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  171. package/src/lib/server/daemon/types.ts +101 -0
  172. package/src/lib/server/embeddings.ts +3 -9
  173. package/src/lib/server/eval/agent-regression.ts +3 -2
  174. package/src/lib/server/eval/runner.ts +2 -2
  175. package/src/lib/server/execution-brief.test.ts +167 -0
  176. package/src/lib/server/execution-brief.ts +295 -0
  177. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  178. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  179. package/src/lib/server/execution-engine/index.ts +35 -0
  180. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  181. package/src/lib/server/execution-engine/types.ts +33 -0
  182. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  183. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  184. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  185. package/src/lib/server/messages/message-repository.ts +330 -0
  186. package/src/lib/server/missions/mission-service/core.ts +8 -6
  187. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  188. package/src/lib/server/openclaw/doctor.ts +1 -1
  189. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  190. package/src/lib/server/openclaw/gateway.ts +5 -14
  191. package/src/lib/server/openclaw/health.ts +3 -11
  192. package/src/lib/server/openclaw/sync.ts +8 -6
  193. package/src/lib/server/persistence/storage-context.ts +3 -0
  194. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  195. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  196. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  197. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  198. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  199. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  200. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  201. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  202. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  203. package/src/lib/server/protocols/protocol-types.ts +10 -7
  204. package/src/lib/server/provider-endpoint.ts +7 -12
  205. package/src/lib/server/provider-model-discovery.ts +2 -11
  206. package/src/lib/server/query-expansion.ts +5 -6
  207. package/src/lib/server/run-context.test.ts +365 -0
  208. package/src/lib/server/run-context.ts +367 -0
  209. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  210. package/src/lib/server/runtime/queue/core.ts +61 -190
  211. package/src/lib/server/runtime/run-ledger.ts +8 -0
  212. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  213. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  214. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  215. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  216. package/src/lib/server/service-result.ts +16 -0
  217. package/src/lib/server/session-note.ts +2 -3
  218. package/src/lib/server/session-reset-policy.ts +4 -3
  219. package/src/lib/server/session-tools/connector.ts +9 -6
  220. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  221. package/src/lib/server/session-tools/crud.ts +162 -10
  222. package/src/lib/server/session-tools/delegate.ts +1 -1
  223. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  224. package/src/lib/server/session-tools/memory.ts +6 -4
  225. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  226. package/src/lib/server/session-tools/session-info.ts +119 -12
  227. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  228. package/src/lib/server/session-tools/skills.ts +15 -15
  229. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  230. package/src/lib/server/session-tools/subagent.ts +125 -7
  231. package/src/lib/server/session-tools/team-context.ts +4 -3
  232. package/src/lib/server/session-tools/wallet.ts +0 -58
  233. package/src/lib/server/sessions/session-lineage.ts +55 -0
  234. package/src/lib/server/sessions/session-repository.ts +2 -2
  235. package/src/lib/server/skills/learned-skills.ts +24 -23
  236. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  237. package/src/lib/server/skills/skill-repository.ts +136 -13
  238. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  239. package/src/lib/server/storage-normalization.test.ts +44 -267
  240. package/src/lib/server/storage-normalization.ts +75 -0
  241. package/src/lib/server/storage.ts +19 -0
  242. package/src/lib/server/structured-extract.ts +3 -14
  243. package/src/lib/server/tasks/task-followups.ts +16 -11
  244. package/src/lib/server/tasks/task-result.test.ts +25 -29
  245. package/src/lib/server/tasks/task-result.ts +5 -9
  246. package/src/lib/server/tasks/task-route-service.ts +449 -0
  247. package/src/lib/server/text-normalization.ts +41 -0
  248. package/src/lib/server/tool-planning.ts +6 -42
  249. package/src/lib/server/upload-path.ts +5 -0
  250. package/src/lib/server/working-state/extraction.ts +614 -0
  251. package/src/lib/server/working-state/normalization.ts +866 -0
  252. package/src/lib/server/working-state/prompt.ts +60 -0
  253. package/src/lib/server/working-state/repository.ts +38 -0
  254. package/src/lib/server/working-state/service.test.ts +253 -0
  255. package/src/lib/server/working-state/service.ts +293 -0
  256. package/src/lib/validation/schemas.ts +1 -0
  257. package/src/lib/ws-client.ts +3 -3
  258. package/src/stores/slices/task-slice.ts +1 -4
  259. package/src/stores/use-chatroom-store.ts +2 -2
  260. package/src/types/index.ts +277 -12
@@ -13,16 +13,17 @@ import { loadSessions, saveSessions } from '@/lib/server/sessions/session-reposi
13
13
  import { loadSettings } from '@/lib/server/settings/settings-repository'
14
14
  import { loadTasks, saveTasks } from '@/lib/server/tasks/task-repository'
15
15
  import { notify } from '@/lib/server/ws-hub'
16
+ import { getMessages, getLastMessage, appendMessage } from '@/lib/server/messages/message-repository'
16
17
  import { perf } from '@/lib/server/runtime/perf'
17
18
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
18
19
  import { createAgentTaskSession } from '@/lib/server/agents/task-session'
19
20
  import { formatValidationFailure } from '@/lib/server/tasks/task-validation'
20
21
  import { pushMainLoopEventToMainSessions } from '@/lib/server/agents/main-agent-loop'
21
- import { executeSessionChatTurn, type ExecuteChatTurnResult } from '@/lib/server/chat-execution/chat-execution'
22
+ import type { ExecuteChatTurnResult } from '@/lib/server/chat-execution/chat-execution-types'
22
23
  import { checkAgentBudgetLimits } from '@/lib/server/cost'
24
+ import { enqueueExecution } from '@/lib/server/execution-engine'
23
25
  import { extractTaskResult, formatResultBody } from '@/lib/server/tasks/task-result'
24
26
  import {
25
- assessAutonomyRun,
26
27
  classifyRuntimeFailure,
27
28
  observeAutonomyRunOutcome,
28
29
  recordSupervisorIncident,
@@ -40,7 +41,7 @@ import {
40
41
  } from '@/lib/server/tasks/task-followups'
41
42
  import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
42
43
  import { cascadeUnblock } from '@/lib/server/dag-validation'
43
- import { captureGuardianCheckpoint, prepareGuardianRecovery } from '@/lib/server/agents/guardian'
44
+ import { prepareGuardianRecovery } from '@/lib/server/agents/guardian'
44
45
  import { notifyOrchestrators } from '@/lib/server/runtime/orchestrator-events'
45
46
  import type { Agent, BoardTask, Message, Session } from '@/types'
46
47
  import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agents/agent-availability'
@@ -549,9 +550,9 @@ function collectTaskResultDeliveryData(
549
550
  ): TaskResultDeliveryData {
550
551
  const runSessionId = typeof task.sessionId === 'string' ? task.sessionId : ''
551
552
  const runSession = runSessionId ? sessions[runSessionId] : null
552
- const fallbackText = runSession ? latestAssistantText(runSession) : ''
553
+ const fallbackText = runSessionId ? latestAssistantText(runSessionId) : ''
553
554
  const taskResult = extractTaskResult(
554
- runSession,
555
+ runSessionId ? getMessages(runSessionId) : [],
555
556
  task.result || fallbackText || null,
556
557
  { sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
557
558
  )
@@ -606,10 +607,11 @@ function buildTaskTerminalMessage(
606
607
  return parts.join('\n\n')
607
608
  }
608
609
 
609
- function latestAssistantText(session: SessionLike | null | undefined): string {
610
- if (!Array.isArray(session?.messages)) return ''
611
- for (let i = session.messages.length - 1; i >= 0; i--) {
612
- const msg = session.messages[i]
610
+ function latestAssistantText(sessionId: string | null | undefined): string {
611
+ if (!sessionId) return ''
612
+ const messages = getMessages(sessionId)
613
+ for (let i = messages.length - 1; i >= 0; i--) {
614
+ const msg = messages[i]
613
615
  if (msg?.role !== 'assistant') continue
614
616
  const text = typeof msg?.text === 'string' ? msg.text.trim() : ''
615
617
  if (!text) continue
@@ -619,23 +621,6 @@ function latestAssistantText(session: SessionLike | null | undefined): string {
619
621
  return ''
620
622
  }
621
623
 
622
- // Task result extraction now uses Zod-validated structured data
623
- // from ./task-result.ts (extractTaskResult, formatResultBody)
624
-
625
- /** Check if a task result looks incomplete (agent stopped mid-objective). */
626
- function looksIncomplete(text: string): boolean {
627
- if (!text) return false
628
- const trimmed = text.trim()
629
- // Ends with ellipsis or continuation signal
630
- if (trimmed.endsWith('...') || trimmed.endsWith('…')) return true
631
- // Ends with a step/phase header (agent was listing next steps)
632
- if (/(?:^|\n)#{1,3}\s+(?:Step|Phase|Next)\s+\d/i.test(trimmed.slice(-200))) return true
633
- // Contains forward-looking language at the end
634
- const lastChunk = trimmed.slice(-300).toLowerCase()
635
- if (/\b(?:next i(?:'ll| will)|now i(?:'ll| will)|let me (?:now|next)|moving on to|proceeding to)\b/.test(lastChunk)) return true
636
- return false
637
- }
638
-
639
624
  function queueTaskAutonomyObservation(input: {
640
625
  runId: string
641
626
  sessionId: string
@@ -663,128 +648,6 @@ function queueTaskAutonomyObservation(input: {
663
648
  })
664
649
  }
665
650
 
666
- async function executeTaskRun(
667
- task: BoardTask,
668
- agent: Agent,
669
- sessionId: string,
670
- ): Promise<ExecuteChatTurnResult> {
671
- if (agent.autoRecovery) {
672
- const cwd = task.projectId
673
- ? path.join(WORKSPACE_DIR, 'projects', task.projectId)
674
- : WORKSPACE_DIR
675
- captureGuardianCheckpoint(cwd, `task:${task.id}`)
676
- }
677
- const settings = loadSettings()
678
- const basePrompt = task.description || task.title
679
- const prompt = [
680
- basePrompt,
681
- '',
682
- 'Completion requirements:',
683
- '- Execute the task before replying; do not reply with only a plan.',
684
- '- Include concrete evidence in your final summary: changed file paths, commands run, and verification results.',
685
- '- If blocked, state the blocker explicitly and what input or permission is missing.',
686
- ].join('\n')
687
- // All agents go through the unified chat execution path.
688
- // Agents with delegation enabled get delegation tools automatically via session-tools.
689
- let latestRun: ExecuteChatTurnResult = await executeSessionChatTurn({
690
- sessionId,
691
- message: prompt,
692
- internal: false,
693
- source: 'task',
694
- runId: task.id,
695
- })
696
- let text = typeof latestRun.text === 'string' ? latestRun.text.trim() : ''
697
- let previousSummary: string | null = null
698
- let totalInputTokens = latestRun.inputTokens || 0
699
- let totalOutputTokens = latestRun.outputTokens || 0
700
- let totalEstimatedCost = Number(latestRun.estimatedCost || 0)
701
- if (latestRun.error) {
702
- return {
703
- ...latestRun,
704
- text,
705
- }
706
- }
707
-
708
- const maxSupervisorFollowups = 2
709
- for (let followupIndex = 0; followupIndex < maxSupervisorFollowups; followupIndex += 1) {
710
- const sessions = loadSessions()
711
- const session = sessions[sessionId] as unknown as Session | undefined
712
- const assessment = assessAutonomyRun({
713
- runId: `${task.id}:attempt-${(task.attempts || 0) + 1}:step-${followupIndex + 1}`,
714
- sessionId,
715
- taskId: task.id,
716
- agentId: agent.id,
717
- source: 'task',
718
- status: latestRun.error ? 'failed' : 'completed',
719
- resultText: text,
720
- error: latestRun.error,
721
- toolEvents: latestRun.toolEvents,
722
- mainLoopState: {
723
- followupChainCount: followupIndex + 1,
724
- summary: previousSummary,
725
- missionCostUsd: totalEstimatedCost,
726
- },
727
- session: session || null,
728
- settings,
729
- })
730
- if (assessment.shouldBlock) break
731
- if (assessment.autoActions?.length) {
732
- const { executeSupervisorAutoActions } = await import('@/lib/server/autonomy/supervisor-reflection')
733
- const result = await executeSupervisorAutoActions({
734
- actions: assessment.autoActions,
735
- sessionId,
736
- agentId: agent?.id,
737
- })
738
- if (result.blocked) break
739
- }
740
- const followupMessage = assessment.interventionPrompt
741
- || (text && looksIncomplete(text)
742
- ? 'Continue and complete the remaining steps. Provide a final summary when done.'
743
- : null)
744
- if (!followupMessage) break
745
-
746
- // Budget check before follow-up
747
- const typedAgentForBudget = agent as Agent
748
- if (typedAgentForBudget.monthlyBudget || typedAgentForBudget.dailyBudget || typedAgentForBudget.hourlyBudget) {
749
- try {
750
- const followupBudget = checkAgentBudgetLimits(typedAgentForBudget)
751
- if (!followupBudget.ok) {
752
- log.warn(TAG, `[queue] Budget exceeded for "${typedAgentForBudget.name}" during follow-up, stopping.`)
753
- break
754
- }
755
- } catch {}
756
- }
757
-
758
- previousSummary = text || previousSummary
759
- const followUp = await executeSessionChatTurn({
760
- sessionId,
761
- message: followupMessage,
762
- internal: false,
763
- source: 'task',
764
- })
765
- totalInputTokens += followUp.inputTokens || 0
766
- totalOutputTokens += followUp.outputTokens || 0
767
- totalEstimatedCost += Number(followUp.estimatedCost || 0)
768
- text = typeof followUp.text === 'string' ? followUp.text.trim() : ''
769
- latestRun = {
770
- ...followUp,
771
- text,
772
- inputTokens: totalInputTokens,
773
- outputTokens: totalOutputTokens,
774
- estimatedCost: totalEstimatedCost,
775
- }
776
- if (latestRun.error) break
777
- }
778
-
779
- return {
780
- ...latestRun,
781
- text,
782
- inputTokens: totalInputTokens,
783
- outputTokens: totalOutputTokens,
784
- estimatedCost: totalEstimatedCost,
785
- }
786
- }
787
-
788
651
  function hasFinishedExecutionSession(session: SessionLike | Session | null | undefined): boolean {
789
652
  if (!session) return false
790
653
  return session.active === false && !session.currentRunId
@@ -810,7 +673,7 @@ export function reconcileFinishedRunningTasks(): { reconciled: number; deadLette
810
673
  const session = sessions[sessionId]
811
674
  if (!hasFinishedExecutionSession(session)) continue
812
675
 
813
- const fallbackText = latestAssistantText(session)
676
+ const fallbackText = latestAssistantText(sessionId)
814
677
  if (!fallbackText && !task.result) {
815
678
  task.status = 'failed'
816
679
  task.result = 'Agent session finished without producing output.'
@@ -821,7 +684,7 @@ export function reconcileFinishedRunningTasks(): { reconciled: number; deadLette
821
684
 
822
685
  applyTaskPolicyDefaults(task)
823
686
  const taskResult = extractTaskResult(
824
- session,
687
+ getMessages(sessionId),
825
688
  task.result || fallbackText || null,
826
689
  { sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
827
690
  )
@@ -930,8 +793,7 @@ function pushUserFacingTaskResult(task: BoardTask, sessions: Record<string, Sess
930
793
  const taskLink = `[${task.title}](#task:${task.id})`
931
794
  const body = buildTaskTerminalMessage(`Task ${delivery.statusLabel}: **${taskLink}**`, task, delivery)
932
795
  const now = Date.now()
933
- if (!Array.isArray(targetSession.messages)) targetSession.messages = []
934
- const lastMsg = targetSession.messages.at(-1)
796
+ const lastMsg = getLastMessage(targetSessionId)
935
797
  if (lastMsg?.role === 'assistant' && lastMsg?.text === body && typeof lastMsg?.time === 'number' && now - lastMsg.time < 30_000) {
936
798
  return
937
799
  }
@@ -943,9 +805,7 @@ function pushUserFacingTaskResult(task: BoardTask, sessions: Record<string, Sess
943
805
  kind: 'system',
944
806
  }
945
807
  if (delivery.firstImage) message.imageUrl = delivery.firstImage.url
946
- targetSession.messages.push(message)
947
- targetSession.lastActiveAt = now
948
- saveSessions(sessions as Record<string, Session>)
808
+ appendMessage(targetSessionId, message)
949
809
  notify(`messages:${targetSessionId}`)
950
810
  }
951
811
 
@@ -1455,32 +1315,33 @@ export async function processNext() {
1455
1315
  })
1456
1316
 
1457
1317
  // Save initial assistant message so user sees context when opening the session
1458
- const sessions = loadSessions()
1459
- if (sessions[sessionId]) {
1460
- const isDelegation = (task as unknown as Record<string, unknown>).sourceType === 'delegation'
1461
- let initialText: string
1462
- if (isDelegation) {
1463
- const delegatorId = (task as unknown as Record<string, unknown>).delegatedByAgentId as string | undefined
1464
- const delegator = delegatorId ? agents[delegatorId] : null
1465
- const prefix = `[delegation-source:${delegatorId || ''}:${delegator?.name || 'Agent'}:${delegator?.avatarSeed || ''}]`
1466
- initialText = `${prefix}\nDelegated by **${delegator?.name || 'another agent'}** | [${task.title}](#task:${task.id})\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`${buildTaskContinuationNote(Boolean(reusedExistingSession), resumeContext)}\n\nI'll begin working on this now.`
1467
- } else {
1468
- initialText = `Starting task: **${task.title}**\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`${buildTaskContinuationNote(Boolean(reusedExistingSession), resumeContext)}\n\nI'll begin working on this now.`
1469
- }
1470
- // Inject upstream task results context
1471
- if (Array.isArray(task.upstreamResults) && task.upstreamResults.length > 0) {
1472
- const upstreamBlock = task.upstreamResults
1473
- .map((ur) => `### ${ur.taskTitle}\n${ur.resultPreview || '(no result)'}`)
1474
- .join('\n\n')
1475
- initialText += `\n\n## Context from upstream tasks\n\n${upstreamBlock}`
1318
+ {
1319
+ const sessionExists = Boolean(loadSessions()[sessionId])
1320
+ if (sessionExists) {
1321
+ const isDelegation = (task as unknown as Record<string, unknown>).sourceType === 'delegation'
1322
+ let initialText: string
1323
+ if (isDelegation) {
1324
+ const delegatorId = (task as unknown as Record<string, unknown>).delegatedByAgentId as string | undefined
1325
+ const delegator = delegatorId ? agents[delegatorId] : null
1326
+ const prefix = `[delegation-source:${delegatorId || ''}:${delegator?.name || 'Agent'}:${delegator?.avatarSeed || ''}]`
1327
+ initialText = `${prefix}\nDelegated by **${delegator?.name || 'another agent'}** | [${task.title}](#task:${task.id})\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`${buildTaskContinuationNote(Boolean(reusedExistingSession), resumeContext)}\n\nI'll begin working on this now.`
1328
+ } else {
1329
+ initialText = `Starting task: **${task.title}**\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`${buildTaskContinuationNote(Boolean(reusedExistingSession), resumeContext)}\n\nI'll begin working on this now.`
1330
+ }
1331
+ // Inject upstream task results context
1332
+ if (Array.isArray(task.upstreamResults) && task.upstreamResults.length > 0) {
1333
+ const upstreamBlock = task.upstreamResults
1334
+ .map((ur) => `### ${ur.taskTitle}\n${ur.resultPreview || '(no result)'}`)
1335
+ .join('\n\n')
1336
+ initialText += `\n\n## Context from upstream tasks\n\n${upstreamBlock}`
1337
+ }
1338
+ appendMessage(sessionId, {
1339
+ role: 'assistant',
1340
+ text: initialText,
1341
+ time: Date.now(),
1342
+ ...(isDelegation ? { kind: 'system' as const } : {}),
1343
+ })
1476
1344
  }
1477
- sessions[sessionId].messages.push({
1478
- role: 'assistant',
1479
- text: initialText,
1480
- time: Date.now(),
1481
- ...(isDelegation ? { kind: 'system' as const } : {}),
1482
- })
1483
- saveSessions(sessions)
1484
1345
  }
1485
1346
 
1486
1347
  log.info(TAG, `[queue] Running task "${task.title}" (${taskId}) with ${agent.name}`)
@@ -1488,7 +1349,14 @@ export async function processNext() {
1488
1349
  try {
1489
1350
  const taskRunId = `${taskId}:attempt-${(task.attempts || 0) + 1}`
1490
1351
  const endTaskRunPerf = perf.start('queue', 'executeTaskRun', { taskId, agentName: agent.name })
1491
- const taskRun = await executeTaskRun(task, agent, sessionId)
1352
+ const taskRunHandle = enqueueExecution({
1353
+ kind: 'task_attempt',
1354
+ task,
1355
+ agent,
1356
+ sessionId,
1357
+ executionId: taskRunId,
1358
+ })
1359
+ const taskRun = await taskRunHandle.promise
1492
1360
  endTaskRunPerf()
1493
1361
  // Update lastActivityAt after execution completes (idle timeout tracking)
1494
1362
  {
@@ -1524,9 +1392,8 @@ export async function processNext() {
1524
1392
  if (t2[taskId]) {
1525
1393
  applyTaskPolicyDefaults(t2[taskId])
1526
1394
  // Structured extraction: Zod-validated result with typed artifacts
1527
- const runSessions = loadSessions()
1528
1395
  const taskResult = extractTaskResult(
1529
- runSessions[sessionId],
1396
+ getMessages(sessionId),
1530
1397
  result || null,
1531
1398
  { sinceTime: typeof t2[taskId].startedAt === 'number' ? t2[taskId].startedAt : null },
1532
1399
  )
@@ -1723,13 +1590,17 @@ export async function processNext() {
1723
1590
  t2[taskId].repairRunId = repairRunId
1724
1591
  t2[taskId].lastRepairAttemptAt = Date.now()
1725
1592
  saveTasks(t2)
1726
- await executeSessionChatTurn({
1727
- sessionId: t2[taskId].sessionId!,
1728
- message: `[AUTO-REPAIR] ${failureClassification.repairPrompt}\n\nOriginal error: ${errMsg.slice(0, 300)}`,
1729
- internal: true,
1730
- source: 'task-repair',
1731
- runId: repairRunId,
1732
- })
1593
+ await enqueueExecution({
1594
+ kind: 'session_turn',
1595
+ input: {
1596
+ sessionId: t2[taskId].sessionId!,
1597
+ message: `[AUTO-REPAIR] ${failureClassification.repairPrompt}\n\nOriginal error: ${errMsg.slice(0, 300)}`,
1598
+ internal: true,
1599
+ source: 'task-repair',
1600
+ mode: 'followup',
1601
+ dedupeKey: repairRunId,
1602
+ },
1603
+ }).promise
1733
1604
  log.info(TAG, `[queue] Repair turn completed for task "${task.title}" (${taskId})`)
1734
1605
  } catch (repairErr: unknown) {
1735
1606
  log.warn(TAG, `[queue] Repair turn failed for task "${task.title}":`, repairErr instanceof Error ? repairErr.message : String(repairErr))
@@ -73,6 +73,10 @@ export function listPersistedRuns(params?: {
73
73
  export function appendPersistedRunEvent(input: {
74
74
  runId: string
75
75
  sessionId: string
76
+ kind?: RunEventRecord['kind']
77
+ ownerType?: RunEventRecord['ownerType']
78
+ ownerId?: RunEventRecord['ownerId']
79
+ parentExecutionId?: RunEventRecord['parentExecutionId']
76
80
  phase: 'status' | 'event'
77
81
  status?: SessionRunStatus
78
82
  event: SSEEvent
@@ -86,6 +90,10 @@ export function appendPersistedRunEvent(input: {
86
90
  id: genId(12),
87
91
  runId: input.runId,
88
92
  sessionId: input.sessionId,
93
+ kind: input.kind,
94
+ ownerType: input.ownerType,
95
+ ownerId: input.ownerId,
96
+ parentExecutionId: input.parentExecutionId,
89
97
  timestamp,
90
98
  phase: input.phase,
91
99
  status: input.status,
@@ -1,4 +1,4 @@
1
- import { executeSessionChatTurn } from '@/lib/server/chat-execution/chat-execution'
1
+ import { executeExecutionChatTurn } from '@/lib/server/execution-engine/chat-turn'
2
2
  import { log } from '@/lib/server/logger'
3
3
  import { isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
4
4
  import { notify } from '@/lib/server/ws-hub'
@@ -98,7 +98,7 @@ export async function drainExecution(
98
98
  }
99
99
 
100
100
  try {
101
- const result = await executeSessionChatTurn({
101
+ const result = await executeExecutionChatTurn({
102
102
  sessionId: next.run.sessionId,
103
103
  message: next.message,
104
104
  imagePath: next.imagePath,
@@ -6,6 +6,7 @@ import { log } from '@/lib/server/logger'
6
6
  import { isInternalHeartbeatRun } from '@/lib/server/runtime/heartbeat-source'
7
7
  import { getEnabledToolIds } from '@/lib/capability-selection'
8
8
  import { isAllEstopEngaged, isAutonomyEstopEngaged } from '@/lib/server/runtime/estop'
9
+ import { isRestartRecoverableSource } from '@/lib/server/runtime/run-ledger'
9
10
  import { getActiveSessionProcess } from '@/lib/server/runtime/runtime-state'
10
11
 
11
12
  import { cancelPendingForSession } from './cancellation'
@@ -215,6 +216,11 @@ export function enqueueSessionRun(
215
216
  id: runId,
216
217
  sessionId: input.sessionId,
217
218
  missionId: input.missionId ?? getSession(input.sessionId)?.missionId ?? null,
219
+ kind: 'session_turn',
220
+ ownerType: 'session',
221
+ ownerId: input.sessionId,
222
+ parentExecutionId: null,
223
+ recoveryPolicy: isRestartRecoverableSource(source) ? 'restart_recoverable' : 'ephemeral',
218
224
  source,
219
225
  internal,
220
226
  mode,
@@ -118,6 +118,10 @@ export function persistEventForRun(entry: SessionRunQueueEntry, event: SSEEvent,
118
118
  appendPersistedRunEvent({
119
119
  runId: entry.run.id,
120
120
  sessionId: entry.run.sessionId,
121
+ kind: entry.run.kind,
122
+ ownerType: entry.run.ownerType,
123
+ ownerId: entry.run.ownerId,
124
+ parentExecutionId: entry.run.parentExecutionId,
121
125
  phase: opts?.phase || 'event',
122
126
  status: opts?.status,
123
127
  summary: opts?.summary,
@@ -0,0 +1,230 @@
1
+ import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agents/agent-availability'
2
+ import { loadAgents } from '@/lib/server/agents/agent-repository'
3
+ import { logActivity } from '@/lib/server/activity/activity-log'
4
+ import { enqueueTask } from '@/lib/server/runtime/queue'
5
+ import { loadSessions } from '@/lib/server/sessions/session-repository'
6
+ import { prepareScheduleUpdate, prepareScheduleCreate } from '@/lib/server/schedules/schedule-service'
7
+ import {
8
+ archiveScheduleCluster,
9
+ purgeArchivedScheduleCluster,
10
+ restoreArchivedScheduleCluster,
11
+ } from '@/lib/server/schedules/schedule-lifecycle'
12
+ import { loadSchedule, loadSchedules, upsertSchedule, upsertSchedules } from '@/lib/server/schedules/schedule-repository'
13
+ import { serviceFail, serviceOk } from '@/lib/server/service-result'
14
+ import { errorMessage } from '@/lib/shared-utils'
15
+ import { getScheduleSignatureKey } from '@/lib/schedules/schedule-dedupe'
16
+ import { prepareScheduledTaskRun } from '@/lib/server/tasks/task-lifecycle'
17
+ import { loadTasks, saveTask } from '@/lib/server/tasks/task-repository'
18
+ import { pushMainLoopEventToMainSessions } from '@/lib/server/agents/main-agent-loop'
19
+ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
20
+ import { notify } from '@/lib/server/ws-hub'
21
+ import type { Schedule } from '@/types'
22
+ import type { ScheduleLike } from '@/lib/schedules/schedule-dedupe'
23
+ import type { ServiceResult } from '@/lib/server/service-result'
24
+
25
+ type InFlightTask = {
26
+ status?: string
27
+ sourceScheduleKey?: string | null
28
+ }
29
+
30
+ export function listSchedulesForApi(includeArchived: boolean) {
31
+ const schedules = loadSchedules()
32
+ if (includeArchived) return schedules
33
+ const filtered: typeof schedules = {}
34
+ for (const [id, schedule] of Object.entries(schedules)) {
35
+ if (schedule.status === 'archived') continue
36
+ filtered[id] = schedule
37
+ }
38
+ return filtered
39
+ }
40
+
41
+ export function createScheduleFromRoute(body: Record<string, unknown>): ServiceResult<ScheduleLike> {
42
+ const now = Date.now()
43
+ const schedules = loadSchedules()
44
+ const agents = loadAgents()
45
+ const candidateAgentId = typeof body.agentId === 'string' ? body.agentId.trim() : ''
46
+ const agent = agents[candidateAgentId]
47
+ if (!agent) {
48
+ return serviceFail(400, `Agent not found: ${String(body.agentId)}`)
49
+ }
50
+ if (isAgentDisabled(agent)) {
51
+ return serviceFail(409, buildAgentDisabledMessage(agent, 'take scheduled work'))
52
+ }
53
+ const prepared = prepareScheduleCreate({
54
+ input: body,
55
+ schedules,
56
+ now,
57
+ cwd: WORKSPACE_DIR,
58
+ })
59
+ if (!prepared.ok) {
60
+ return serviceFail(400, prepared.error)
61
+ }
62
+ if (prepared.kind === 'duplicate') {
63
+ if (prepared.entries.length === 1) upsertSchedule(prepared.scheduleId, prepared.schedule)
64
+ else if (prepared.entries.length > 1) upsertSchedules(prepared.entries)
65
+ if (prepared.entries.length > 0) notify('schedules')
66
+ return serviceOk(prepared.schedule)
67
+ }
68
+ upsertSchedule(prepared.scheduleId, prepared.schedule)
69
+ logActivity({
70
+ entityType: 'schedule',
71
+ entityId: prepared.scheduleId,
72
+ action: 'created',
73
+ actor: 'user',
74
+ summary: `Schedule created: "${prepared.schedule.name}"`,
75
+ })
76
+ notify('schedules')
77
+ return serviceOk(prepared.schedule)
78
+ }
79
+
80
+ export function updateScheduleFromRoute(id: string, body: Record<string, unknown>): ServiceResult<ScheduleLike & { [key: string]: unknown }> {
81
+ const schedules = loadSchedules()
82
+ const current = schedules[id]
83
+ if (!current) return serviceFail(404, 'Schedule not found')
84
+
85
+ if (body.restore === true) {
86
+ const restored = restoreArchivedScheduleCluster(id, {
87
+ actor: { actor: 'user' },
88
+ })
89
+ if (!restored.ok || !restored.schedule) {
90
+ return serviceFail(409, 'Schedule is not archived.')
91
+ }
92
+ return serviceOk({
93
+ ...restored.schedule,
94
+ restoredIds: restored.restoredIds,
95
+ })
96
+ }
97
+
98
+ if (body.status === 'archived') {
99
+ const archived = archiveScheduleCluster(id, {
100
+ actor: { actor: 'user' },
101
+ })
102
+ if (!archived.ok || !archived.schedule) {
103
+ return serviceFail(500, 'Failed to archive schedule.')
104
+ }
105
+ return serviceOk({
106
+ ...archived.schedule,
107
+ archivedIds: archived.archivedIds,
108
+ cancelledTaskIds: archived.cancelledTaskIds,
109
+ abortedRunSessionIds: archived.abortedRunSessionIds,
110
+ })
111
+ }
112
+
113
+ const sessions = loadSessions()
114
+ const agents = loadAgents()
115
+ const sessionCwd = typeof current.createdInSessionId === 'string'
116
+ ? sessions[current.createdInSessionId]?.cwd
117
+ : null
118
+ const prepared = prepareScheduleUpdate({
119
+ id,
120
+ current,
121
+ patch: body,
122
+ schedules,
123
+ now: Date.now(),
124
+ cwd: sessionCwd || WORKSPACE_DIR,
125
+ agentExists: (agentId) => Boolean(agents[agentId]),
126
+ propagateEquivalentStatuses: true,
127
+ propagationSource: current as unknown as Record<string, unknown>,
128
+ })
129
+ if (!prepared.ok) {
130
+ return serviceFail(400, errorMessage(prepared.error))
131
+ }
132
+ upsertSchedules(prepared.entries)
133
+ logActivity({
134
+ entityType: 'schedule',
135
+ entityId: id,
136
+ action: 'updated',
137
+ actor: 'user',
138
+ summary: `Schedule updated: "${prepared.schedule.name}"`,
139
+ detail: prepared.affectedScheduleIds.length > 1 ? { affectedScheduleIds: prepared.affectedScheduleIds } : undefined,
140
+ })
141
+ notify('schedules')
142
+ return serviceOk(
143
+ prepared.affectedScheduleIds.length > 1
144
+ ? { ...prepared.schedule, affectedScheduleIds: prepared.affectedScheduleIds }
145
+ : prepared.schedule,
146
+ )
147
+ }
148
+
149
+ export function deleteScheduleFromRoute(id: string, purge: boolean): ServiceResult<Record<string, unknown>> {
150
+ const current = loadSchedule(id)
151
+ if (!current) return serviceFail(404, 'Schedule not found')
152
+ if (purge) {
153
+ const purged = purgeArchivedScheduleCluster(id, {
154
+ actor: { actor: 'user' },
155
+ })
156
+ if (!purged.ok) {
157
+ return serviceFail(409, 'Only archived schedules can be purged.')
158
+ }
159
+ return serviceOk({ ok: true, purgedIds: purged.purgedIds })
160
+ }
161
+ const archived = archiveScheduleCluster(id, {
162
+ actor: { actor: 'user' },
163
+ })
164
+ if (!archived.ok || !archived.schedule) {
165
+ return serviceFail(500, 'Failed to archive schedule.')
166
+ }
167
+ return serviceOk({
168
+ ok: true,
169
+ archivedIds: archived.archivedIds,
170
+ cancelledTaskIds: archived.cancelledTaskIds,
171
+ removedQueuedTaskIds: archived.removedQueuedTaskIds,
172
+ abortedRunSessionIds: archived.abortedRunSessionIds,
173
+ schedule: archived.schedule,
174
+ })
175
+ }
176
+
177
+ export function runScheduleNow(id: string): ServiceResult<Record<string, unknown>> {
178
+ const schedule = loadSchedule(id) as Schedule | null
179
+ if (!schedule) return serviceFail(404, 'Schedule not found')
180
+ if (schedule.status === 'archived') {
181
+ return serviceFail(409, 'Archived schedules must be restored before they can run.')
182
+ }
183
+
184
+ const agents = loadAgents()
185
+ const agent = agents[schedule.agentId]
186
+ if (!agent) return serviceFail(400, 'Agent not found')
187
+ if (isAgentDisabled(agent)) {
188
+ return serviceFail(409, buildAgentDisabledMessage(agent, 'run schedules'))
189
+ }
190
+
191
+ const tasks = loadTasks()
192
+ const scheduleSignature = getScheduleSignatureKey(schedule)
193
+ if (scheduleSignature) {
194
+ const inFlight = Object.values(tasks as Record<string, InFlightTask>).some((task) =>
195
+ task
196
+ && (task.status === 'queued' || task.status === 'running')
197
+ && task.sourceScheduleKey === scheduleSignature
198
+ )
199
+ if (inFlight) {
200
+ return serviceOk({ ok: true, queued: false, reason: 'in_flight' })
201
+ }
202
+ }
203
+
204
+ const now = Date.now()
205
+ schedule.runNumber = (schedule.runNumber || 0) + 1
206
+ const { taskId } = prepareScheduledTaskRun({
207
+ schedule,
208
+ tasks,
209
+ now,
210
+ scheduleSignature,
211
+ })
212
+ saveTask(taskId, tasks[taskId])
213
+ enqueueTask(taskId)
214
+ pushMainLoopEventToMainSessions({
215
+ type: 'schedule_fired',
216
+ text: `Schedule fired manually: "${schedule.name}" (${schedule.id}) run #${schedule.runNumber} — task ${taskId}`,
217
+ })
218
+ schedule.lastRunAt = now
219
+ upsertSchedule(schedule.id, schedule)
220
+ logActivity({
221
+ entityType: 'schedule',
222
+ entityId: schedule.id,
223
+ action: 'started',
224
+ actor: 'user',
225
+ summary: `Schedule run started: "${schedule.name}"`,
226
+ detail: { taskId, runNumber: schedule.runNumber },
227
+ })
228
+
229
+ return serviceOk({ ok: true, queued: true, taskId, runNumber: schedule.runNumber })
230
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Standardized result type for route-facing services.
3
+ *
4
+ * Routes inspect `ok` to decide the HTTP status code and response body.
5
+ */
6
+ export type ServiceResult<T> =
7
+ | { ok: true; payload: T }
8
+ | { ok: false; status: number; payload: { error: string } }
9
+
10
+ export function serviceOk<T>(payload: T): ServiceResult<T> {
11
+ return { ok: true, payload }
12
+ }
13
+
14
+ export function serviceFail(status: number, error: string): ServiceResult<never> {
15
+ return { ok: false, status, payload: { error } }
16
+ }