@swarmclawai/swarmclaw 1.2.0 → 1.2.2

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 (241) hide show
  1. package/README.md +19 -0
  2. package/package.json +5 -2
  3. package/skills/coding-agent/SKILL.md +111 -0
  4. package/skills/github/SKILL.md +140 -0
  5. package/skills/nano-banana-pro/SKILL.md +62 -0
  6. package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
  7. package/skills/nano-pdf/SKILL.md +53 -0
  8. package/skills/openai-image-gen/SKILL.md +78 -0
  9. package/skills/openai-image-gen/scripts/gen.py +328 -0
  10. package/skills/resourceful-problem-solving/SKILL.md +49 -0
  11. package/skills/skill-creator/SKILL.md +147 -0
  12. package/skills/skill-creator/scripts/init_skill.py +378 -0
  13. package/skills/skill-creator/scripts/quick_validate.py +159 -0
  14. package/skills/summarize/SKILL.md +77 -0
  15. package/src/app/api/auth/route.ts +20 -5
  16. package/src/app/api/chats/[id]/deploy/route.ts +11 -6
  17. package/src/app/api/chats/[id]/devserver/route.ts +17 -20
  18. package/src/app/api/chats/[id]/messages/route.ts +15 -11
  19. package/src/app/api/chats/[id]/route.ts +9 -10
  20. package/src/app/api/chats/[id]/stop/route.ts +5 -7
  21. package/src/app/api/chats/messages-route.test.ts +8 -6
  22. package/src/app/api/chats/route.ts +9 -10
  23. package/src/app/api/credentials/[id]/route.ts +4 -1
  24. package/src/app/api/extensions/marketplace/route.ts +5 -2
  25. package/src/app/api/ip/route.ts +2 -2
  26. package/src/app/api/memory/maintenance/route.ts +5 -2
  27. package/src/app/api/preview-server/route.ts +15 -12
  28. package/src/app/api/projects/[id]/route.ts +7 -46
  29. package/src/app/api/system/status/route.ts +11 -0
  30. package/src/app/api/upload/route.ts +4 -1
  31. package/src/cli/index.js +7 -0
  32. package/src/cli/spec.js +1 -0
  33. package/src/components/agents/agent-files-editor.tsx +44 -32
  34. package/src/components/agents/personality-builder.tsx +13 -7
  35. package/src/components/agents/trash-list.tsx +1 -1
  36. package/src/components/chat/chat-area.tsx +45 -23
  37. package/src/components/chat/message-bubble.test.ts +35 -0
  38. package/src/components/chat/message-bubble.tsx +20 -9
  39. package/src/components/chat/message-list.tsx +62 -42
  40. package/src/components/chat/swarm-status-card.tsx +10 -3
  41. package/src/components/input/chat-input.tsx +34 -14
  42. package/src/components/layout/daemon-indicator.tsx +7 -8
  43. package/src/components/layout/update-banner.tsx +8 -13
  44. package/src/components/logs/log-list.tsx +1 -1
  45. package/src/components/memory/memory-card.tsx +3 -1
  46. package/src/components/org-chart/org-chart-view.tsx +4 -0
  47. package/src/components/projects/project-list.tsx +4 -2
  48. package/src/components/projects/tabs/overview-tab.tsx +3 -2
  49. package/src/components/secrets/secret-sheet.tsx +1 -1
  50. package/src/components/secrets/secrets-list.tsx +1 -1
  51. package/src/components/shared/agent-switch-dialog.tsx +12 -6
  52. package/src/components/shared/dir-browser.tsx +22 -18
  53. package/src/components/skills/skill-sheet.tsx +2 -3
  54. package/src/components/tasks/task-list.tsx +1 -1
  55. package/src/components/tasks/task-sheet.tsx +1 -1
  56. package/src/hooks/use-openclaw-gateway.ts +46 -27
  57. package/src/instrumentation.ts +10 -7
  58. package/src/lib/chat/assistant-render-id.ts +3 -0
  59. package/src/lib/chat/chat-streaming-state.test.ts +42 -3
  60. package/src/lib/chat/chat-streaming-state.ts +20 -8
  61. package/src/lib/chat/chat.ts +18 -2
  62. package/src/lib/chat/queued-message-queue.test.ts +23 -1
  63. package/src/lib/chat/queued-message-queue.ts +11 -2
  64. package/src/lib/providers/anthropic.ts +6 -3
  65. package/src/lib/providers/claude-cli.ts +9 -3
  66. package/src/lib/providers/cli-utils.test.ts +124 -0
  67. package/src/lib/providers/cli-utils.ts +15 -0
  68. package/src/lib/providers/codex-cli.ts +9 -3
  69. package/src/lib/providers/gemini-cli.ts +6 -2
  70. package/src/lib/providers/index.ts +4 -1
  71. package/src/lib/providers/ollama.ts +5 -2
  72. package/src/lib/providers/openai.ts +8 -5
  73. package/src/lib/providers/opencode-cli.ts +6 -2
  74. package/src/lib/server/activity/activity-log.ts +21 -0
  75. package/src/lib/server/agents/agent-availability.test.ts +10 -5
  76. package/src/lib/server/agents/agent-cascade.ts +79 -59
  77. package/src/lib/server/agents/agent-registry.ts +23 -4
  78. package/src/lib/server/agents/agent-repository.ts +90 -0
  79. package/src/lib/server/agents/delegation-job-repository.ts +53 -0
  80. package/src/lib/server/agents/delegation-jobs.ts +11 -4
  81. package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
  82. package/src/lib/server/agents/guardian.ts +2 -2
  83. package/src/lib/server/agents/main-agent-loop.ts +14 -6
  84. package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
  85. package/src/lib/server/agents/subagent-runtime.ts +9 -6
  86. package/src/lib/server/agents/subagent-swarm.ts +3 -2
  87. package/src/lib/server/agents/task-session.ts +3 -4
  88. package/src/lib/server/approvals/approval-repository.ts +30 -0
  89. package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
  90. package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
  91. package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
  92. package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
  93. package/src/lib/server/chat-execution/chat-execution.ts +84 -1914
  94. package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
  95. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
  96. package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
  97. package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
  98. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
  99. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
  100. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
  101. package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
  102. package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
  103. package/src/lib/server/chat-execution/message-classifier.ts +5 -2
  104. package/src/lib/server/chat-execution/post-stream-finalization.ts +5 -2
  105. package/src/lib/server/chat-execution/prompt-builder.ts +22 -1
  106. package/src/lib/server/chat-execution/prompt-sections.ts +55 -13
  107. package/src/lib/server/chat-execution/response-completeness.ts +5 -2
  108. package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
  109. package/src/lib/server/chat-execution/stream-agent-chat.ts +58 -25
  110. package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
  111. package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
  112. package/src/lib/server/connectors/bluebubbles.ts +7 -4
  113. package/src/lib/server/connectors/connector-inbound.ts +16 -13
  114. package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
  115. package/src/lib/server/connectors/connector-outbound.ts +6 -3
  116. package/src/lib/server/connectors/connector-repository.ts +58 -0
  117. package/src/lib/server/connectors/discord.ts +10 -7
  118. package/src/lib/server/connectors/email.ts +17 -14
  119. package/src/lib/server/connectors/googlechat.ts +7 -4
  120. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
  121. package/src/lib/server/connectors/matrix.ts +6 -3
  122. package/src/lib/server/connectors/openclaw.ts +20 -17
  123. package/src/lib/server/connectors/outbox.ts +4 -1
  124. package/src/lib/server/connectors/runtime-state.test.ts +117 -0
  125. package/src/lib/server/connectors/runtime-state.ts +19 -0
  126. package/src/lib/server/connectors/session-consolidation.ts +5 -2
  127. package/src/lib/server/connectors/signal.ts +9 -6
  128. package/src/lib/server/connectors/slack.ts +13 -10
  129. package/src/lib/server/connectors/teams.ts +8 -5
  130. package/src/lib/server/connectors/telegram.ts +15 -12
  131. package/src/lib/server/connectors/whatsapp.ts +32 -29
  132. package/src/lib/server/credentials/credential-repository.ts +7 -0
  133. package/src/lib/server/embeddings.ts +4 -1
  134. package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
  135. package/src/lib/server/link-understanding.ts +4 -1
  136. package/src/lib/server/memory/memory-abstract.test.ts +59 -0
  137. package/src/lib/server/memory/memory-abstract.ts +59 -0
  138. package/src/lib/server/memory/memory-db.ts +40 -14
  139. package/src/lib/server/missions/mission-repository.ts +74 -0
  140. package/src/lib/server/missions/mission-service/actions.ts +6 -0
  141. package/src/lib/server/missions/mission-service/bindings.ts +9 -0
  142. package/src/lib/server/missions/mission-service/context.ts +4 -0
  143. package/src/lib/server/missions/mission-service/core.ts +2269 -0
  144. package/src/lib/server/missions/mission-service/queries.ts +12 -0
  145. package/src/lib/server/missions/mission-service/recovery.ts +5 -0
  146. package/src/lib/server/missions/mission-service/ticks.ts +9 -0
  147. package/src/lib/server/missions/mission-service.test.ts +9 -2
  148. package/src/lib/server/missions/mission-service.ts +6 -2263
  149. package/src/lib/server/openclaw/gateway.ts +8 -5
  150. package/src/lib/server/persistence/repository-utils.ts +154 -0
  151. package/src/lib/server/persistence/storage-context.ts +51 -0
  152. package/src/lib/server/persistence/transaction.ts +1 -0
  153. package/src/lib/server/project-utils.ts +13 -0
  154. package/src/lib/server/projects/project-repository.ts +36 -0
  155. package/src/lib/server/projects/project-service.ts +79 -0
  156. package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
  157. package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
  158. package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
  159. package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
  160. package/src/lib/server/provider-health.ts +18 -0
  161. package/src/lib/server/query-expansion.ts +4 -1
  162. package/src/lib/server/runtime/alert-dispatch.ts +8 -7
  163. package/src/lib/server/runtime/daemon-policy.ts +1 -1
  164. package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
  165. package/src/lib/server/runtime/daemon-state/health.ts +6 -0
  166. package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
  167. package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
  168. package/src/lib/server/runtime/daemon-state.test.ts +48 -0
  169. package/src/lib/server/runtime/daemon-state.ts +3 -1331
  170. package/src/lib/server/runtime/estop-repository.ts +4 -0
  171. package/src/lib/server/runtime/estop.ts +3 -1
  172. package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
  173. package/src/lib/server/runtime/heartbeat-service.ts +78 -34
  174. package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
  175. package/src/lib/server/runtime/idle-window.ts +6 -3
  176. package/src/lib/server/runtime/network.ts +11 -0
  177. package/src/lib/server/runtime/orchestrator-events.ts +2 -2
  178. package/src/lib/server/runtime/perf.ts +4 -1
  179. package/src/lib/server/runtime/process-manager.ts +7 -4
  180. package/src/lib/server/runtime/queue/claims.ts +4 -0
  181. package/src/lib/server/runtime/queue/core.ts +2079 -0
  182. package/src/lib/server/runtime/queue/execution.ts +7 -0
  183. package/src/lib/server/runtime/queue/followups.ts +4 -0
  184. package/src/lib/server/runtime/queue/queries.ts +12 -0
  185. package/src/lib/server/runtime/queue/recovery.ts +7 -0
  186. package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
  187. package/src/lib/server/runtime/queue-repository.ts +17 -0
  188. package/src/lib/server/runtime/queue.ts +5 -2058
  189. package/src/lib/server/runtime/run-ledger.ts +6 -5
  190. package/src/lib/server/runtime/run-repository.ts +73 -0
  191. package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
  192. package/src/lib/server/runtime/runtime-settings.ts +1 -1
  193. package/src/lib/server/runtime/runtime-state.ts +99 -0
  194. package/src/lib/server/runtime/scheduler.ts +13 -8
  195. package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
  196. package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
  197. package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
  198. package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
  199. package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
  200. package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
  201. package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
  202. package/src/lib/server/runtime/session-run-manager.ts +72 -1374
  203. package/src/lib/server/runtime/watch-job-repository.ts +35 -0
  204. package/src/lib/server/runtime/watch-jobs.ts +3 -1
  205. package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
  206. package/src/lib/server/sandbox/novnc-auth.ts +10 -0
  207. package/src/lib/server/schedules/schedule-repository.ts +42 -0
  208. package/src/lib/server/session-tools/context.ts +14 -0
  209. package/src/lib/server/session-tools/discovery.ts +9 -6
  210. package/src/lib/server/session-tools/index.ts +3 -1
  211. package/src/lib/server/session-tools/platform.ts +1 -1
  212. package/src/lib/server/session-tools/subagent.ts +23 -2
  213. package/src/lib/server/session-tools/wallet.ts +4 -1
  214. package/src/lib/server/sessions/session-repository.ts +85 -0
  215. package/src/lib/server/settings/settings-repository.ts +25 -0
  216. package/src/lib/server/skills/clawhub-client.ts +4 -1
  217. package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
  218. package/src/lib/server/skills/skill-discovery.test.ts +2 -2
  219. package/src/lib/server/skills/skill-discovery.ts +2 -2
  220. package/src/lib/server/skills/skill-eligibility.ts +6 -0
  221. package/src/lib/server/skills/skill-repository.ts +14 -0
  222. package/src/lib/server/solana.ts +6 -0
  223. package/src/lib/server/storage-auth.ts +5 -5
  224. package/src/lib/server/storage-normalization.ts +4 -0
  225. package/src/lib/server/storage.ts +32 -32
  226. package/src/lib/server/tasks/task-followups.ts +4 -1
  227. package/src/lib/server/tasks/task-repository.ts +54 -0
  228. package/src/lib/server/tool-loop-detection.ts +8 -3
  229. package/src/lib/server/tool-planning.ts +226 -0
  230. package/src/lib/server/tool-retry.ts +4 -3
  231. package/src/lib/server/usage/usage-repository.ts +30 -0
  232. package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
  233. package/src/lib/server/webhooks/webhook-repository.ts +10 -0
  234. package/src/lib/server/ws-hub.ts +5 -2
  235. package/src/lib/strip-internal-metadata.test.ts +78 -37
  236. package/src/lib/strip-internal-metadata.ts +20 -6
  237. package/src/stores/use-approval-store.ts +7 -1
  238. package/src/stores/use-chat-store.test.ts +54 -0
  239. package/src/stores/use-chat-store.ts +26 -6
  240. package/src/types/index.ts +6 -0
  241. /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
@@ -1,2263 +1,6 @@
1
- import { genId } from '@/lib/id'
2
- import type {
3
- ApprovalRequest,
4
- BoardTask,
5
- DelegationJobRecord,
6
- MessageToolEvent,
7
- Mission,
8
- MissionEvent,
9
- MissionPhase,
10
- MissionSource,
11
- MissionSourceRef,
12
- MissionStatus,
13
- MissionSummary,
14
- MissionVerificationVerdict,
15
- Schedule,
16
- Session,
17
- SessionQueuedTurn,
18
- SessionRunRecord,
19
- } from '@/types'
20
- import {
21
- loadApprovals,
22
- loadDelegationJobs,
23
- loadMission,
24
- loadMissionEvents,
25
- loadMissions,
26
- loadSettings,
27
- loadSession,
28
- loadTask,
29
- loadTasks,
30
- patchMission,
31
- patchSession,
32
- patchTask,
33
- releaseRuntimeLock,
34
- renewRuntimeLock,
35
- tryAcquireRuntimeLock,
36
- upsertSchedule,
37
- upsertMission,
38
- upsertMissionEvent,
39
- logActivity,
40
- } from '@/lib/server/storage'
41
- import {
42
- classifyMissionTurn,
43
- planMissionTick,
44
- verifyMissionOutcome,
45
- type MissionOutcomeDecision,
46
- type MissionPlannerDecisionResult,
47
- type MissionTurnDecision,
48
- } from '@/lib/server/missions/mission-intent'
49
- import { errorMessage, hmrSingleton } from '@/lib/shared-utils'
50
- import { getSessionQueueSnapshot, listRuns } from '@/lib/server/runtime/session-run-manager'
51
- import { notify } from '@/lib/server/ws-hub'
52
-
53
- function now(): number {
54
- return Date.now()
55
- }
56
-
57
- function cleanText(value: unknown, max = 320): string {
58
- return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim().slice(0, max) : ''
59
- }
60
-
61
- function uniqueStrings(values: unknown, maxItems: number, maxChars = 180): string[] {
62
- const source = Array.isArray(values) ? values : []
63
- const out: string[] = []
64
- const seen = new Set<string>()
65
- for (const entry of source) {
66
- const normalized = cleanText(entry, maxChars)
67
- if (!normalized) continue
68
- const key = normalized.toLowerCase()
69
- if (seen.has(key)) continue
70
- seen.add(key)
71
- out.push(normalized)
72
- if (out.length >= maxItems) break
73
- }
74
- return out
75
- }
76
-
77
- const MISSION_LEASE_TTL_MS = 15_000
78
- const MISSION_LEASE_OWNER = `mission:${process.pid}:${genId(6)}`
79
- const recoveryState = hmrSingleton('__swarmclaw_mission_controller_recovery__', () => ({ completed: false }))
80
-
81
- function areMissionHumanLoopWaitsEnabled(): boolean {
82
- const settings = loadSettings() as { missionHumanLoopEnabled?: unknown }
83
- return settings.missionHumanLoopEnabled === true
84
- }
85
-
86
- function shouldSuppressMissionHumanLoopWait(waitKind: unknown): boolean {
87
- return waitKind === 'human_reply' && !areMissionHumanLoopWaitsEnabled()
88
- }
89
-
90
- function isMissionTerminal(status: MissionStatus): boolean {
91
- return status === 'completed' || status === 'failed' || status === 'cancelled'
92
- }
93
-
94
- function missionLeaseName(missionId: string): string {
95
- return `mission:${missionId}`
96
- }
97
-
98
- function listMissionIds(value: unknown, maxItems = 128): string[] {
99
- return uniqueStrings(value, maxItems, 48)
100
- }
101
-
102
- function pickMissionPhase(value: unknown, fallback: MissionPhase = 'planning'): MissionPhase {
103
- const phase = typeof value === 'string' ? value.trim().toLowerCase() : ''
104
- if (phase === 'intake' || phase === 'planning' || phase === 'dispatching' || phase === 'executing' || phase === 'verifying' || phase === 'waiting' || phase === 'completed' || phase === 'failed') {
105
- return phase
106
- }
107
- return fallback
108
- }
109
-
110
- function pickMissionWaitKind(value: unknown): NonNullable<Mission['waitState']>['kind'] {
111
- const kind = typeof value === 'string' ? value.trim().toLowerCase() : ''
112
- if (kind === 'human_reply' || kind === 'approval' || kind === 'external_dependency' || kind === 'provider' || kind === 'blocked_task' || kind === 'blocked_mission' || kind === 'scheduled') {
113
- return kind
114
- }
115
- return 'other'
116
- }
117
-
118
- function normalizeMissionSourceRef(source: MissionSource, mission: Partial<Mission>): MissionSourceRef {
119
- const sourceRef = mission.sourceRef
120
- if (sourceRef && typeof sourceRef === 'object' && 'kind' in sourceRef) return sourceRef
121
- if (source === 'schedule' && typeof (mission as { sourceScheduleId?: string | null }).sourceScheduleId === 'string') {
122
- return {
123
- kind: 'schedule',
124
- scheduleId: (mission as { sourceScheduleId?: string | null }).sourceScheduleId || '',
125
- recurring: true,
126
- }
127
- }
128
- if ((source === 'chat' || source === 'connector' || source === 'heartbeat' || source === 'main-loop-followup') && typeof mission.sessionId === 'string' && mission.sessionId.trim()) {
129
- return source === 'connector'
130
- ? { kind: 'connector', sessionId: mission.sessionId.trim(), connectorId: '', channelId: '' }
131
- : source === 'heartbeat'
132
- ? { kind: 'heartbeat', sessionId: mission.sessionId.trim() }
133
- : { kind: 'chat', sessionId: mission.sessionId.trim() }
134
- }
135
- if (source === 'task' && typeof mission.rootTaskId === 'string' && mission.rootTaskId.trim()) {
136
- return { kind: 'task', taskId: mission.rootTaskId.trim() }
137
- }
138
- return { kind: 'manual' }
139
- }
140
-
141
- function normalizeMissionRecord(mission: Mission): Mission {
142
- const rootMissionId = typeof mission.rootMissionId === 'string' && mission.rootMissionId.trim()
143
- ? mission.rootMissionId.trim()
144
- : mission.id
145
- const parentMissionId = typeof mission.parentMissionId === 'string' && mission.parentMissionId.trim()
146
- ? mission.parentMissionId.trim()
147
- : null
148
- const controllerState = mission.controllerState && typeof mission.controllerState === 'object'
149
- ? { ...mission.controllerState }
150
- : {}
151
- const plannerState = mission.plannerState && typeof mission.plannerState === 'object'
152
- ? { ...mission.plannerState }
153
- : {}
154
- const verificationState = mission.verificationState && typeof mission.verificationState === 'object'
155
- ? { ...mission.verificationState }
156
- : { candidate: false }
157
- return {
158
- ...mission,
159
- phase: pickMissionPhase(mission.phase),
160
- sourceRef: normalizeMissionSourceRef(mission.source, mission),
161
- rootMissionId,
162
- ...(parentMissionId ? { parentMissionId } : {}),
163
- childMissionIds: listMissionIds(mission.childMissionIds, 256),
164
- dependencyMissionIds: listMissionIds(mission.dependencyMissionIds, 256),
165
- dependencyTaskIds: listMissionIds(mission.dependencyTaskIds, 256),
166
- taskIds: listMissionIds(mission.taskIds, 256),
167
- controllerState,
168
- plannerState,
169
- verificationState: {
170
- candidate: verificationState.candidate === true,
171
- requiredTaskIds: listMissionIds(verificationState.requiredTaskIds, 128),
172
- requiredChildMissionIds: listMissionIds(verificationState.requiredChildMissionIds, 128),
173
- requiredArtifacts: uniqueStrings(verificationState.requiredArtifacts, 128, 240),
174
- evidenceSummary: cleanText(verificationState.evidenceSummary, 320) || null,
175
- lastVerdict: ((): MissionVerificationVerdict | null => {
176
- const verdict = typeof verificationState.lastVerdict === 'string' ? verificationState.lastVerdict.trim().toLowerCase() : ''
177
- return verdict === 'continue' || verdict === 'waiting' || verdict === 'completed' || verdict === 'failed' || verdict === 'replan'
178
- ? verdict
179
- : null
180
- })(),
181
- lastVerifiedAt: typeof verificationState.lastVerifiedAt === 'number' ? verificationState.lastVerifiedAt : null,
182
- },
183
- waitState: mission.waitState
184
- ? {
185
- kind: pickMissionWaitKind(mission.waitState.kind),
186
- reason: cleanText(mission.waitState.reason, 220) || 'Mission is waiting.',
187
- approvalId: typeof mission.waitState.approvalId === 'string' ? mission.waitState.approvalId : null,
188
- untilAt: typeof mission.waitState.untilAt === 'number' ? mission.waitState.untilAt : null,
189
- dependencyTaskId: typeof mission.waitState.dependencyTaskId === 'string' ? mission.waitState.dependencyTaskId : null,
190
- dependencyMissionId: typeof mission.waitState.dependencyMissionId === 'string' ? mission.waitState.dependencyMissionId : null,
191
- providerKey: typeof mission.waitState.providerKey === 'string' ? mission.waitState.providerKey : null,
192
- }
193
- : null,
194
- }
195
- }
196
-
197
- function missionSourceFromTask(task: BoardTask, fallback: MissionSource = 'manual'): MissionSource {
198
- if (task.sourceType === 'schedule') return 'schedule'
199
- if (task.sourceType === 'delegation') return 'delegation'
200
- return fallback
201
- }
202
-
203
- export function loadMissionById(id: string | null | undefined): Mission | null {
204
- ensureMissionControllerRecovered()
205
- const missionId = typeof id === 'string' ? id.trim() : ''
206
- if (!missionId) return null
207
- const mission = loadMission(missionId)
208
- return mission ? normalizeMissionRecord(mission) : null
209
- }
210
-
211
- export function findLatestMissionForSession(sessionId: string): Mission | null {
212
- const missions = Object.values(loadMissions())
213
- .map((mission) => normalizeMissionRecord(mission))
214
- .filter((mission) => mission.sessionId === sessionId)
215
- .sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0))
216
- const active = missions.find((mission) => !isMissionTerminal(mission.status))
217
- return active || missions[0] || null
218
- }
219
-
220
- export function getMissionForSession(session: Session | null | undefined): Mission | null {
221
- if (!session) return null
222
- const byId = loadMissionById(session.missionId)
223
- if (byId) return byId
224
- return findLatestMissionForSession(session.id)
225
- }
226
-
227
- function listTaskSummaries(taskIds: string[] | undefined): Array<{
228
- id: string
229
- title: string
230
- status: string
231
- result?: string | null
232
- error?: string | null
233
- }> {
234
- const tasks = loadTasks()
235
- const source = Array.isArray(taskIds) ? taskIds : []
236
- return source
237
- .map((taskId) => tasks[taskId])
238
- .filter((task): task is BoardTask => Boolean(task))
239
- .map((task) => ({
240
- id: task.id,
241
- title: task.title,
242
- status: task.status,
243
- result: task.result || null,
244
- error: task.error || null,
245
- }))
246
- }
247
-
248
- export function buildMissionSummary(mission: Mission): MissionSummary {
249
- const taskSummaries = listTaskSummaries(mission.taskIds)
250
- const completedTaskCount = taskSummaries.filter((task) => task.status === 'completed').length
251
- const openTaskCount = taskSummaries.filter((task) => !['completed', 'failed', 'cancelled', 'archived'].includes(task.status)).length
252
- return {
253
- id: mission.id,
254
- objective: mission.objective,
255
- status: mission.status,
256
- phase: mission.phase,
257
- source: mission.source,
258
- currentStep: mission.currentStep || null,
259
- waitingReason: mission.waitState?.reason || null,
260
- sessionId: mission.sessionId || null,
261
- agentId: mission.agentId || null,
262
- projectId: mission.projectId || null,
263
- parentMissionId: mission.parentMissionId || null,
264
- rootMissionId: mission.rootMissionId || mission.id,
265
- taskIds: Array.isArray(mission.taskIds) ? mission.taskIds : [],
266
- openTaskCount,
267
- completedTaskCount,
268
- childCount: Array.isArray(mission.childMissionIds) ? mission.childMissionIds.length : 0,
269
- sourceRef: mission.sourceRef,
270
- updatedAt: mission.updatedAt,
271
- }
272
- }
273
-
274
- export function enrichSessionWithMissionSummary<T extends Session>(session: T): T {
275
- const mission = getMissionForSession(session)
276
- if (!mission) return { ...session, missionSummary: null } as T
277
- return {
278
- ...session,
279
- missionId: mission.id,
280
- missionSummary: buildMissionSummary(mission),
281
- } as T
282
- }
283
-
284
- export function enrichTaskWithMissionSummary<T extends BoardTask>(task: T): T {
285
- const mission = loadMissionById(task.missionId)
286
- if (!mission) return { ...task, missionSummary: null } as T
287
- return {
288
- ...task,
289
- missionSummary: buildMissionSummary(mission),
290
- } as T
291
- }
292
-
293
- export function listMissionEventsForMission(missionId: string, limit = 200): MissionEvent[] {
294
- ensureMissionControllerRecovered()
295
- const safeLimit = Math.max(1, Math.min(2000, Math.trunc(limit)))
296
- return Object.values(loadMissionEvents())
297
- .filter((event) => event.missionId === missionId)
298
- .sort((left, right) => left.createdAt - right.createdAt)
299
- .slice(-safeLimit)
300
- }
301
-
302
- export function listMissions(options?: {
303
- sessionId?: string | null
304
- status?: MissionStatus | 'non_terminal'
305
- phase?: MissionPhase | null
306
- source?: MissionSource | null
307
- agentId?: string | null
308
- projectId?: string | null
309
- parentMissionId?: string | null
310
- limit?: number
311
- }): Mission[] {
312
- ensureMissionControllerRecovered()
313
- const missions = Object.values(loadMissions())
314
- .map((mission) => normalizeMissionRecord(mission))
315
- .filter((mission) => {
316
- if (options?.sessionId && mission.sessionId !== options.sessionId) return false
317
- if (!options?.status) return true
318
- if (options.status === 'non_terminal') return !isMissionTerminal(mission.status)
319
- return mission.status === options.status
320
- })
321
- .filter((mission) => !options?.phase || mission.phase === options.phase)
322
- .filter((mission) => !options?.source || mission.source === options.source)
323
- .filter((mission) => !options?.agentId || mission.agentId === options.agentId)
324
- .filter((mission) => !options?.projectId || mission.projectId === options.projectId)
325
- .filter((mission) => !options?.parentMissionId || mission.parentMissionId === options.parentMissionId)
326
- .sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0))
327
-
328
- const limit = typeof options?.limit === 'number'
329
- ? Math.max(1, Math.min(500, Math.trunc(options.limit)))
330
- : null
331
- return limit ? missions.slice(0, limit) : missions
332
- }
333
-
334
- export function listChildMissions(parentMissionId: string, limit?: number): Mission[] {
335
- const missions = listMissions({ parentMissionId })
336
- if (typeof limit !== 'number') return missions
337
- return missions.slice(0, Math.max(1, Math.trunc(limit)))
338
- }
339
-
340
- function listMissionApprovals(mission: Mission): ApprovalRequest[] {
341
- const approvals = Object.values(loadApprovals()) as ApprovalRequest[]
342
- return approvals
343
- .filter((approval) =>
344
- approval.id === mission.waitState?.approvalId
345
- || (typeof approval.sessionId === 'string' && approval.sessionId === mission.sessionId)
346
- )
347
- .sort((left, right) => (right.createdAt || 0) - (left.createdAt || 0))
348
- }
349
-
350
- function listMissionQueuedTurns(mission: Mission): SessionQueuedTurn[] {
351
- const queue = mission.sessionId ? getSessionQueueSnapshot(mission.sessionId) : null
352
- if (!queue) return []
353
- return queue.items.filter((item) => item.missionId === mission.id)
354
- }
355
-
356
- function listMissionRuns(mission: Mission, limit = 20): SessionRunRecord[] {
357
- return listRuns({ limit: Math.max(20, limit * 4) })
358
- .filter((run) => run.missionId === mission.id)
359
- .slice(0, limit)
360
- }
361
-
362
- function listRecentMissionEvents(missionId: string, limit = 12): MissionEvent[] {
363
- return listMissionEventsForMission(missionId, limit)
364
- }
365
-
366
- function hasTerminalMissionEvidence(mission: Mission): boolean {
367
- const requiredTaskIds = mission.verificationState?.requiredTaskIds || mission.taskIds || []
368
- const requiredChildMissionIds = mission.verificationState?.requiredChildMissionIds || mission.childMissionIds || []
369
- const tasks = loadTasks()
370
- const requiredTasksSatisfied = requiredTaskIds.every((taskId) => {
371
- const task = tasks[taskId]
372
- return Boolean(task && task.status === 'completed')
373
- })
374
- const requiredChildrenSatisfied = requiredChildMissionIds.every((childId) => {
375
- const child = loadMissionById(childId)
376
- return Boolean(child && child.status === 'completed')
377
- })
378
- return requiredTasksSatisfied && requiredChildrenSatisfied
379
- }
380
-
381
- function missionNeedsStartupRecovery(mission: Mission): boolean {
382
- if (isMissionTerminal(mission.status)) return false
383
- if (mission.status === 'waiting') return false
384
- return mission.phase === 'dispatching' || mission.phase === 'executing' || mission.phase === 'verifying'
385
- }
386
-
387
- function recoverMissionOnStartup(mission: Mission): { mission: Mission | null; rerunVerification: boolean } {
388
- const reconciled = reconcileMissionState(mission)
389
- if (!missionNeedsStartupRecovery(reconciled)) return { mission: loadMissionById(reconciled.id) || reconciled, rerunVerification: false }
390
- const hasLiveExecution = missionHasActiveTask(reconciled) || missionHasActiveRun(reconciled) || missionHasActiveChild(reconciled)
391
- if (hasLiveExecution) return { mission: loadMissionById(reconciled.id) || reconciled, rerunVerification: false }
392
- if (reconciled.phase === 'verifying' && hasTerminalMissionEvidence(reconciled)) {
393
- const updated = patchMissionStatus(reconciled.id, (current) => ({
394
- ...current,
395
- status: 'active',
396
- phase: 'verifying',
397
- controllerState: {
398
- ...(current.controllerState || {}),
399
- activeRunId: null,
400
- currentTaskId: null,
401
- currentChildMissionId: null,
402
- tickRequestedAt: now(),
403
- tickReason: 'restart_recovery',
404
- },
405
- }))
406
- if (updated) {
407
- appendMissionEvent({
408
- missionId: updated.id,
409
- type: 'interrupted',
410
- source: 'system',
411
- summary: 'Mission verification recovered after restart.',
412
- sessionId: updated.sessionId || null,
413
- runId: updated.lastRunId || null,
414
- data: { phase: mission.phase, recoveredPhase: 'verifying' },
415
- })
416
- }
417
- return { mission: updated, rerunVerification: Boolean(updated) }
418
- }
419
-
420
- const updated = patchMissionStatus(reconciled.id, (current) => ({
421
- ...clearMissionExecutionPointers(current),
422
- status: 'active',
423
- phase: 'planning',
424
- waitState: null,
425
- controllerState: {
426
- ...(current.controllerState || {}),
427
- tickRequestedAt: now(),
428
- tickReason: 'restart_recovery',
429
- },
430
- }))
431
- if (updated) {
432
- appendMissionEvent({
433
- missionId: updated.id,
434
- type: 'interrupted',
435
- source: 'system',
436
- summary: 'Mission execution was interrupted and returned to planning.',
437
- sessionId: updated.sessionId || null,
438
- runId: updated.lastRunId || null,
439
- data: { phase: mission.phase, recoveredPhase: 'planning' },
440
- })
441
- }
442
- return { mission: updated, rerunVerification: Boolean(updated) }
443
- }
444
-
445
- function ensureMissionControllerRecovered(): void {
446
- if (recoveryState.completed) return
447
- recoveryState.completed = true
448
- const rerunTickIds = new Set<string>()
449
- for (const mission of Object.values(loadMissions()).map((entry) => normalizeMissionRecord(entry))) {
450
- if (isMissionTerminal(mission.status)) continue
451
- const recovered = recoverMissionOnStartup(mission)
452
- if (recovered.rerunVerification && recovered.mission?.id) rerunTickIds.add(recovered.mission.id)
453
- }
454
- for (const missionId of rerunTickIds) {
455
- queueMicrotask(() => {
456
- requestMissionTick(missionId, 'restart_recovery', { recovered: true })
457
- })
458
- }
459
- }
460
-
461
- export function getMissionDetail(missionId: string): {
462
- mission: Mission
463
- summary: MissionSummary
464
- parent: MissionSummary | null
465
- children: MissionSummary[]
466
- linkedTasks: BoardTask[]
467
- recentRuns: SessionRunRecord[]
468
- queuedTurns: SessionQueuedTurn[]
469
- approvals: ApprovalRequest[]
470
- events: MissionEvent[]
471
- } | null {
472
- ensureMissionControllerRecovered()
473
- const mission = loadMissionById(missionId)
474
- if (!mission) return null
475
- const tasks = loadTasks()
476
- const parentMission = mission.parentMissionId ? loadMissionById(mission.parentMissionId) : null
477
- return {
478
- mission,
479
- summary: buildMissionSummary(mission),
480
- parent: parentMission ? buildMissionSummary(parentMission) : null,
481
- children: listChildMissions(mission.id).map((child) => buildMissionSummary(child)),
482
- linkedTasks: (mission.taskIds || [])
483
- .map((taskId) => tasks[taskId])
484
- .filter((task): task is BoardTask => Boolean(task))
485
- .map((task) => enrichTaskWithMissionSummary(task)),
486
- recentRuns: listMissionRuns(mission),
487
- queuedTurns: listMissionQueuedTurns(mission),
488
- approvals: listMissionApprovals(mission),
489
- events: listMissionEventsForMission(mission.id, 80),
490
- }
491
- }
492
-
493
- export function appendMissionEvent(input: Omit<MissionEvent, 'id' | 'createdAt'> & { createdAt?: number }): MissionEvent {
494
- const event: MissionEvent = {
495
- id: genId(12),
496
- createdAt: typeof input.createdAt === 'number' ? input.createdAt : now(),
497
- ...input,
498
- }
499
- upsertMissionEvent(event.id, event)
500
- notify('missions')
501
- return event
502
- }
503
-
504
- function ensureMissionTaskLink(mission: Mission, taskId: string): Mission {
505
- const taskIds = uniqueStrings([...(mission.taskIds || []), taskId], 128, 48)
506
- return {
507
- ...mission,
508
- taskIds,
509
- rootTaskId: mission.rootTaskId || taskId,
510
- verificationState: {
511
- candidate: mission.verificationState?.candidate === true,
512
- requiredTaskIds: uniqueStrings([...(mission.verificationState?.requiredTaskIds || []), taskId], 128, 48),
513
- requiredChildMissionIds: listMissionIds(mission.verificationState?.requiredChildMissionIds, 128),
514
- requiredArtifacts: uniqueStrings(mission.verificationState?.requiredArtifacts, 128, 240),
515
- evidenceSummary: mission.verificationState?.evidenceSummary || null,
516
- lastVerdict: mission.verificationState?.lastVerdict || null,
517
- lastVerifiedAt: mission.verificationState?.lastVerifiedAt || null,
518
- },
519
- }
520
- }
521
-
522
- function patchMissionStatus(
523
- missionId: string,
524
- updater: (mission: Mission) => Mission,
525
- ): Mission | null {
526
- const updated = patchMission(missionId, (current) => {
527
- if (!current) return current
528
- return normalizeMissionRecord({
529
- ...updater(current),
530
- updatedAt: now(),
531
- lastActiveAt: now(),
532
- })
533
- })
534
- if (updated) notify('missions')
535
- return updated ? normalizeMissionRecord(updated) : null
536
- }
537
-
538
- export function acquireMissionLease(missionId: string, ttlMs = MISSION_LEASE_TTL_MS): (() => void) | null {
539
- if (!tryAcquireRuntimeLock(missionLeaseName(missionId), MISSION_LEASE_OWNER, ttlMs)) return null
540
- let released = false
541
- return () => {
542
- if (released) return
543
- released = true
544
- releaseRuntimeLock(missionLeaseName(missionId), MISSION_LEASE_OWNER)
545
- }
546
- }
547
-
548
- export function renewMissionLease(missionId: string, ttlMs = MISSION_LEASE_TTL_MS): boolean {
549
- return renewRuntimeLock(missionLeaseName(missionId), MISSION_LEASE_OWNER, ttlMs)
550
- }
551
-
552
- function missionHasActiveTask(mission: Mission): boolean {
553
- const taskId = mission.controllerState?.currentTaskId
554
- if (!taskId) return false
555
- const task = loadTasks()[taskId]
556
- return Boolean(task && (task.status === 'queued' || task.status === 'running'))
557
- }
558
-
559
- function missionHasActiveRun(mission: Mission): boolean {
560
- const runId = mission.controllerState?.activeRunId || mission.lastRunId
561
- if (!runId) return false
562
- const runs = listMissionRuns(mission, 50)
563
- return runs.some((run) => run.id === runId && (run.status === 'queued' || run.status === 'running'))
564
- }
565
-
566
- function missionHasActiveChild(mission: Mission): boolean {
567
- const currentChildMissionId = mission.controllerState?.currentChildMissionId
568
- if (currentChildMissionId) {
569
- const child = loadMissionById(currentChildMissionId)
570
- if (child && !isMissionTerminal(child.status)) return true
571
- }
572
- return (mission.childMissionIds || []).some((childId) => {
573
- const child = loadMissionById(childId)
574
- return Boolean(child && !isMissionTerminal(child.status))
575
- })
576
- }
577
-
578
- function isWaitSatisfied(mission: Mission): boolean {
579
- const waitState = mission.waitState
580
- if (!waitState) return true
581
- if (waitState.approvalId) {
582
- const approval = listMissionApprovals(mission).find((entry) => entry.id === waitState.approvalId)
583
- if (!approval || approval.status === 'pending') return false
584
- }
585
- if (waitState.untilAt && waitState.untilAt > now()) return false
586
- if (waitState.dependencyTaskId) {
587
- const task = loadTasks()[waitState.dependencyTaskId]
588
- if (!task || !['completed', 'failed', 'cancelled', 'archived'].includes(task.status)) return false
589
- }
590
- if (waitState.dependencyMissionId) {
591
- const child = loadMissionById(waitState.dependencyMissionId)
592
- if (!child || !isMissionTerminal(child.status)) return false
593
- }
594
- return true
595
- }
596
-
597
- function clearMissionExecutionPointers(mission: Mission): Mission {
598
- return {
599
- ...mission,
600
- controllerState: {
601
- ...(mission.controllerState || {}),
602
- activeRunId: null,
603
- currentTaskId: null,
604
- currentChildMissionId: null,
605
- },
606
- }
607
- }
608
-
609
- function maybePromoteChildOutcome(mission: Mission): Mission {
610
- const childIds = mission.childMissionIds || []
611
- if (!childIds.length) return mission
612
- const children = childIds.map((childId) => loadMissionById(childId)).filter((child): child is Mission => Boolean(child))
613
- const activeChild = children.find((child) => !isMissionTerminal(child.status))
614
- if (activeChild) {
615
- return {
616
- ...mission,
617
- status: 'waiting',
618
- phase: 'waiting',
619
- waitState: {
620
- kind: 'blocked_mission',
621
- reason: activeChild.waitState?.reason || `Waiting on child mission: ${activeChild.objective}`,
622
- dependencyMissionId: activeChild.id,
623
- },
624
- controllerState: {
625
- ...(mission.controllerState || {}),
626
- currentChildMissionId: activeChild.id,
627
- },
628
- }
629
- }
630
- const failedChild = children.find((child) => child.status === 'failed')
631
- if (failedChild) {
632
- return {
633
- ...mission,
634
- status: 'waiting',
635
- phase: 'waiting',
636
- blockerSummary: failedChild.blockerSummary || failedChild.verifierSummary || `Child mission failed: ${failedChild.objective}`,
637
- waitState: {
638
- kind: 'blocked_mission',
639
- reason: failedChild.blockerSummary || failedChild.verifierSummary || `Child mission failed: ${failedChild.objective}`,
640
- dependencyMissionId: failedChild.id,
641
- },
642
- }
643
- }
644
- return mission
645
- }
646
-
647
- function reconcileMissionState(mission: Mission): Mission {
648
- let next = normalizeMissionRecord(mission)
649
- next = maybePromoteChildOutcome(next)
650
- if (!missionHasActiveTask(next) && !missionHasActiveRun(next) && !missionHasActiveChild(next)) {
651
- next = clearMissionExecutionPointers(next)
652
- }
653
- if (next.status === 'waiting' && isWaitSatisfied(next)) {
654
- next = {
655
- ...next,
656
- status: 'active',
657
- phase: 'planning',
658
- waitState: null,
659
- blockerSummary: null,
660
- }
661
- }
662
- return next
663
- }
664
-
665
- function isAutoMissionSource(source: MissionSource): boolean {
666
- return source === 'schedule' || source === 'heartbeat' || source === 'main-loop-followup' || source === 'delegation'
667
- }
668
-
669
- function buildMissionFollowupMessage(mission: Mission): string {
670
- return [
671
- 'MISSION_CONTROLLER_TICK',
672
- buildMissionContextBlock(mission),
673
- 'Take the single highest-value next step for this mission.',
674
- 'If the mission is blocked on a real dependency, say so plainly.',
675
- 'If the mission is complete, explain the actual completed outcome instead of promising future work.',
676
- ].filter(Boolean).join('\n\n')
677
- }
678
-
679
- function plannerDecisionSummary(
680
- decision: MissionPlannerDecisionResult,
681
- mission: Mission,
682
- ): string {
683
- const explicit = cleanText((decision as { summary?: string | null }).summary, 360)
684
- if (explicit) return explicit
685
- if (decision.decision === 'dispatch_task') return `Queue linked task ${decision.taskId}.`
686
- if (decision.decision === 'dispatch_session_turn') return 'Queue a mission follow-up turn.'
687
- if (decision.decision === 'spawn_child_mission') return `Create child mission: ${decision.childObjective}`
688
- if (decision.decision === 'wait') return cleanText(decision.waitReason, 220) || 'Mission is waiting.'
689
- if (decision.decision === 'verify_now') return 'Verify mission completion from current durable evidence.'
690
- if (decision.decision === 'complete_candidate') return `Mission looks complete and should enter verification: ${mission.objective}`
691
- if (decision.decision === 'fail_terminal') return `Mission failed: ${mission.objective}`
692
- return 'Mission replanned.'
693
- }
694
-
695
- function areMissionDependenciesSatisfied(mission: Mission): { satisfied: boolean; blockerSummary: string | null } {
696
- const depMissionIds = Array.isArray(mission.dependencyMissionIds) ? mission.dependencyMissionIds : []
697
- for (const depId of depMissionIds) {
698
- const dep = loadMissionById(depId)
699
- if (!dep || !isMissionTerminal(dep.status) || dep.status !== 'completed') {
700
- return { satisfied: false, blockerSummary: `Blocked by mission: ${dep?.objective || depId} (${dep?.status || 'not found'})` }
701
- }
702
- }
703
- const depTaskIds = Array.isArray(mission.dependencyTaskIds) ? mission.dependencyTaskIds : []
704
- for (const depId of depTaskIds) {
705
- const dep = loadTask(depId)
706
- if (!dep || dep.status !== 'completed') {
707
- return { satisfied: false, blockerSummary: `Blocked by task: ${dep?.title || depId} (${dep?.status || 'not found'})` }
708
- }
709
- }
710
- return { satisfied: true, blockerSummary: null }
711
- }
712
-
713
- function deterministicPlannerDecision(mission: Mission): MissionPlannerDecisionResult | null {
714
- // Check external dependencies (dependencyMissionIds / dependencyTaskIds)
715
- const depCheck = areMissionDependenciesSatisfied(mission)
716
- if (!depCheck.satisfied) {
717
- return {
718
- decision: 'wait',
719
- confidence: 1,
720
- summary: depCheck.blockerSummary || 'Blocked by unsatisfied dependency.',
721
- waitKind: 'blocked_mission',
722
- waitReason: depCheck.blockerSummary || 'Blocked by unsatisfied dependency.',
723
- }
724
- }
725
-
726
- const tasks = listTaskSummaries(mission.taskIds)
727
- const failedTask = tasks.find((task) => task.status === 'failed')
728
- if (failedTask) {
729
- return {
730
- decision: 'wait',
731
- confidence: 1,
732
- summary: failedTask.error || `Waiting on failed task: ${failedTask.title}`,
733
- waitKind: 'blocked_task',
734
- waitReason: failedTask.error || `Waiting on failed task: ${failedTask.title}`,
735
- }
736
- }
737
-
738
- const nonTerminalChild = (mission.childMissionIds || [])
739
- .map((childId) => loadMissionById(childId))
740
- .find((child): child is Mission => Boolean(child && !isMissionTerminal(child.status)))
741
- if (nonTerminalChild) {
742
- return {
743
- decision: 'wait',
744
- confidence: 1,
745
- summary: nonTerminalChild.waitState?.reason || `Waiting on child mission: ${nonTerminalChild.objective}`,
746
- waitKind: 'blocked_mission',
747
- waitReason: nonTerminalChild.waitState?.reason || `Waiting on child mission: ${nonTerminalChild.objective}`,
748
- }
749
- }
750
-
751
- const completedTasks = tasks.filter((task) => task.status === 'completed')
752
- const hasTerminalTaskSet = tasks.length > 0 && completedTasks.length === tasks.length
753
- const requiredArtifacts = mission.verificationState?.requiredArtifacts || []
754
- if (hasTerminalTaskSet && requiredArtifacts.length === 0) {
755
- return {
756
- decision: 'verify_now',
757
- confidence: 1,
758
- summary: 'All required linked tasks are complete.',
759
- }
760
- }
761
-
762
- return null
763
- }
764
-
765
- async function planMissionAction(
766
- mission: Mission,
767
- options?: { generateText?: (prompt: string) => Promise<string> },
768
- ): Promise<MissionPlannerDecisionResult> {
769
- const deterministic = deterministicPlannerDecision(mission)
770
- if (deterministic) return deterministic
771
-
772
- const taskSummaries = listTaskSummaries(mission.taskIds)
773
- const childMissionSummaries = listChildMissions(mission.id, 8).map((child) => buildMissionSummary(child))
774
- const queuedTurns = listMissionQueuedTurns(mission)
775
- const recentRuns = listMissionRuns(mission, 8).map((run) => ({
776
- id: run.id,
777
- status: run.status,
778
- source: run.source,
779
- queuedAt: run.queuedAt,
780
- messagePreview: run.messagePreview,
781
- resultPreview: run.resultPreview,
782
- error: run.error,
783
- }))
784
- const recentEvents = listRecentMissionEvents(mission.id, 10).map((event) => ({
785
- type: event.type,
786
- summary: event.summary,
787
- createdAt: event.createdAt,
788
- }))
789
-
790
- const planned = await planMissionTick({
791
- sessionId: mission.sessionId || mission.id,
792
- agentId: mission.agentId || null,
793
- mission,
794
- linkedTaskSummaries: taskSummaries,
795
- childMissionSummaries,
796
- recentRuns,
797
- queuedTurns,
798
- recentEvents,
799
- }, options)
800
-
801
- if (planned) return planned
802
-
803
- if (isAutoMissionSource(mission.source) && mission.sessionId) {
804
- return {
805
- decision: 'dispatch_session_turn',
806
- confidence: 0,
807
- summary: 'Queue a mission follow-up turn using the durable mission context.',
808
- sessionMessage: buildMissionFollowupMessage(mission),
809
- ...(mission.currentStep ? { currentStep: mission.currentStep } : {}),
810
- }
811
- }
812
-
813
- return {
814
- decision: 'replan',
815
- confidence: 0,
816
- summary: 'Mission remains active and is waiting for the next concrete planner decision.',
817
- ...(mission.currentStep ? { currentStep: mission.currentStep } : {}),
818
- }
819
- }
820
-
821
- function applyMissionPlannerPolicies(
822
- mission: Mission,
823
- decision: MissionPlannerDecisionResult,
824
- ): MissionPlannerDecisionResult {
825
- if (decision.decision !== 'wait' || !shouldSuppressMissionHumanLoopWait(decision.waitKind)) return decision
826
- const currentStep = decision.currentStep || mission.currentStep || undefined
827
- if (hasTerminalMissionEvidence(mission) || ((mission.taskIds?.length || 0) === 0 && (mission.childMissionIds?.length || 0) === 0)) {
828
- return {
829
- decision: 'verify_now',
830
- confidence: decision.confidence,
831
- summary: 'Mission human-loop waits are disabled, so the mission will close instead of waiting for another reply.',
832
- ...(currentStep ? { currentStep } : {}),
833
- }
834
- }
835
- return {
836
- decision: 'replan',
837
- confidence: decision.confidence,
838
- summary: 'Mission human-loop waits are disabled, so the mission stays active instead of pausing for another reply.',
839
- ...(currentStep ? { currentStep } : {}),
840
- }
841
- }
842
-
843
- async function executeMissionPlannerDecision(
844
- mission: Mission,
845
- decision: MissionPlannerDecisionResult,
846
- trigger: string,
847
- ): Promise<Mission | null> {
848
- const summary = plannerDecisionSummary(decision, mission)
849
- const basePatch = (updater: (current: Mission) => Mission) => patchMissionStatus(mission.id, (current) => ({
850
- ...updater(current),
851
- plannerState: {
852
- ...(current.plannerState || {}),
853
- lastDecision: decision.decision,
854
- lastPlannedAt: now(),
855
- planSummary: summary,
856
- },
857
- controllerState: {
858
- ...(current.controllerState || {}),
859
- tickRequestedAt: now(),
860
- tickReason: trigger,
861
- },
862
- }))
863
-
864
- appendMissionEvent({
865
- missionId: mission.id,
866
- type: 'planner_decision',
867
- source: 'system',
868
- summary,
869
- sessionId: mission.sessionId || null,
870
- runId: mission.lastRunId || null,
871
- data: {
872
- decision: decision.decision,
873
- trigger,
874
- },
875
- })
876
-
877
- if (decision.decision === 'wait') {
878
- const waitReason = cleanText(decision.waitReason, 220) || summary
879
- const updated = basePatch((current) => ({
880
- ...current,
881
- status: 'waiting',
882
- phase: 'waiting',
883
- waitState: {
884
- kind: decision.waitKind || 'other',
885
- reason: waitReason,
886
- },
887
- blockerSummary: waitReason,
888
- currentStep: decision.currentStep || current.currentStep || null,
889
- }))
890
- if (updated) {
891
- appendMissionEvent({
892
- missionId: updated.id,
893
- type: 'waiting',
894
- source: 'system',
895
- summary: waitReason,
896
- sessionId: updated.sessionId || null,
897
- runId: updated.lastRunId || null,
898
- data: updated.waitState ? updated.waitState as unknown as Record<string, unknown> : null,
899
- })
900
- }
901
- return updated
902
- }
903
-
904
- if (decision.decision === 'complete_candidate') {
905
- return basePatch((current) => ({
906
- ...current,
907
- status: 'active',
908
- phase: 'verifying',
909
- currentStep: decision.currentStep || current.currentStep || null,
910
- verificationState: {
911
- ...(current.verificationState || { candidate: false }),
912
- candidate: true,
913
- evidenceSummary: summary,
914
- },
915
- }))
916
- }
917
-
918
- if (decision.decision === 'verify_now') {
919
- const updated = basePatch((current) => ({
920
- ...current,
921
- status: 'completed',
922
- phase: 'completed',
923
- waitState: null,
924
- blockerSummary: null,
925
- verifierSummary: current.verifierSummary || summary,
926
- currentStep: decision.currentStep || current.currentStep || null,
927
- verificationState: {
928
- ...(current.verificationState || { candidate: false }),
929
- candidate: true,
930
- evidenceSummary: summary,
931
- lastVerdict: 'completed',
932
- lastVerifiedAt: now(),
933
- },
934
- completedAt: current.completedAt || now(),
935
- }))
936
- if (updated) {
937
- appendMissionEvent({
938
- missionId: updated.id,
939
- type: 'verifier_decision',
940
- source: 'system',
941
- summary,
942
- sessionId: updated.sessionId || null,
943
- runId: updated.lastRunId || null,
944
- data: { verdict: 'completed' },
945
- })
946
- appendMissionEvent({
947
- missionId: updated.id,
948
- type: 'completed',
949
- source: 'system',
950
- summary: updated.verifierSummary || summary,
951
- sessionId: updated.sessionId || null,
952
- runId: updated.lastRunId || null,
953
- data: { status: updated.status },
954
- })
955
- if (updated.parentMissionId) noteParentMissionChildOutcome(updated)
956
- }
957
- return updated
958
- }
959
-
960
- if (decision.decision === 'dispatch_task') {
961
- const { enqueueTask } = await import('@/lib/server/runtime/queue')
962
- const task = loadTasks()[decision.taskId]
963
- if (!task) {
964
- return basePatch((current) => ({
965
- ...current,
966
- status: 'waiting',
967
- phase: 'waiting',
968
- waitState: {
969
- kind: 'blocked_task',
970
- reason: `Linked task ${decision.taskId} was not found.`,
971
- },
972
- blockerSummary: `Linked task ${decision.taskId} was not found.`,
973
- }))
974
- }
975
- enqueueTask(decision.taskId)
976
- const updated = basePatch((current) => ({
977
- ...current,
978
- status: 'active',
979
- phase: 'dispatching',
980
- currentStep: decision.currentStep || current.currentStep || task.title || null,
981
- controllerState: {
982
- ...(current.controllerState || {}),
983
- currentTaskId: decision.taskId,
984
- tickRequestedAt: now(),
985
- tickReason: trigger,
986
- },
987
- }))
988
- if (updated) {
989
- appendMissionEvent({
990
- missionId: updated.id,
991
- type: 'dispatch_started',
992
- source: 'system',
993
- summary,
994
- sessionId: updated.sessionId || null,
995
- taskId: decision.taskId,
996
- runId: updated.lastRunId || null,
997
- data: { taskId: decision.taskId },
998
- })
999
- }
1000
- return updated
1001
- }
1002
-
1003
- if (decision.decision === 'dispatch_session_turn') {
1004
- if (!mission.sessionId) {
1005
- return basePatch((current) => ({
1006
- ...current,
1007
- status: 'waiting',
1008
- phase: 'waiting',
1009
- waitState: {
1010
- kind: 'external_dependency',
1011
- reason: 'Mission follow-up needs a linked session before it can continue.',
1012
- },
1013
- blockerSummary: 'Mission follow-up needs a linked session before it can continue.',
1014
- }))
1015
- }
1016
- const { enqueueSessionRun } = await import('@/lib/server/runtime/session-run-manager')
1017
- const queued = enqueueSessionRun({
1018
- sessionId: mission.sessionId || '',
1019
- missionId: mission.id,
1020
- message: decision.sessionMessage,
1021
- internal: true,
1022
- source: 'main-loop-followup',
1023
- mode: 'followup',
1024
- dedupeKey: `mission-tick:${mission.id}`,
1025
- })
1026
- const updated = basePatch((current) => ({
1027
- ...current,
1028
- status: 'active',
1029
- phase: 'dispatching',
1030
- currentStep: decision.currentStep || current.currentStep || null,
1031
- controllerState: {
1032
- ...(current.controllerState || {}),
1033
- activeRunId: queued.runId,
1034
- tickRequestedAt: now(),
1035
- tickReason: trigger,
1036
- },
1037
- }))
1038
- if (updated) {
1039
- appendMissionEvent({
1040
- missionId: updated.id,
1041
- type: 'dispatch_started',
1042
- source: 'system',
1043
- summary,
1044
- sessionId: updated.sessionId || null,
1045
- runId: queued.runId,
1046
- data: { queuedRunId: queued.runId },
1047
- })
1048
- }
1049
- return updated
1050
- }
1051
-
1052
- if (decision.decision === 'spawn_child_mission') {
1053
- const childMission = createMission({
1054
- source: mission.source === 'delegation' ? 'delegation' : 'manual',
1055
- sourceRef: mission.source === 'delegation'
1056
- ? { kind: 'delegation', parentMissionId: mission.id, backend: 'agent' }
1057
- : { kind: 'manual' },
1058
- objective: decision.childObjective,
1059
- successCriteria: decision.childSuccessCriteria,
1060
- currentStep: decision.childCurrentStep || decision.currentStep || null,
1061
- plannerSummary: decision.childPlannerSummary || summary,
1062
- sessionId: mission.sessionId || null,
1063
- agentId: mission.agentId || null,
1064
- projectId: mission.projectId || null,
1065
- parentMissionId: mission.id,
1066
- sourceMessage: decision.childPlannerSummary || decision.childObjective,
1067
- })
1068
- const updated = basePatch((current) => ({
1069
- ...current,
1070
- status: 'waiting',
1071
- phase: 'waiting',
1072
- currentStep: decision.currentStep || current.currentStep || null,
1073
- waitState: {
1074
- kind: 'blocked_mission',
1075
- reason: `Waiting on child mission: ${childMission.objective}`,
1076
- dependencyMissionId: childMission.id,
1077
- },
1078
- controllerState: {
1079
- ...(current.controllerState || {}),
1080
- currentChildMissionId: childMission.id,
1081
- tickRequestedAt: now(),
1082
- tickReason: trigger,
1083
- },
1084
- }))
1085
- if (updated) {
1086
- requestMissionTick(childMission.id, 'child_created', { parentMissionId: mission.id })
1087
- }
1088
- return updated
1089
- }
1090
-
1091
- if (decision.decision === 'fail_terminal') {
1092
- const updated = basePatch((current) => ({
1093
- ...current,
1094
- status: 'failed',
1095
- phase: 'failed',
1096
- blockerSummary: summary,
1097
- verifierSummary: summary,
1098
- failedAt: current.failedAt || now(),
1099
- }))
1100
- if (updated) {
1101
- appendMissionEvent({
1102
- missionId: updated.id,
1103
- type: 'failed',
1104
- source: 'system',
1105
- summary,
1106
- sessionId: updated.sessionId || null,
1107
- runId: updated.lastRunId || null,
1108
- data: { status: updated.status },
1109
- })
1110
- if (updated.parentMissionId) noteParentMissionChildOutcome(updated)
1111
- }
1112
- return updated
1113
- }
1114
-
1115
- return basePatch((current) => ({
1116
- ...current,
1117
- status: 'active',
1118
- phase: 'planning',
1119
- currentStep: decision.currentStep || current.currentStep || null,
1120
- }))
1121
- }
1122
-
1123
- export function requestMissionTick(
1124
- missionId: string,
1125
- trigger: string,
1126
- data?: Record<string, unknown> | null,
1127
- ): Mission | null {
1128
- ensureMissionControllerRecovered()
1129
- const mission = patchMissionStatus(missionId, (current) => ({
1130
- ...reconcileMissionState(current),
1131
- controllerState: {
1132
- ...(current.controllerState || {}),
1133
- tickRequestedAt: now(),
1134
- tickReason: trigger,
1135
- },
1136
- }))
1137
- if (!mission) return null
1138
- appendMissionEvent({
1139
- missionId,
1140
- type: 'source_triggered',
1141
- source: 'system',
1142
- summary: `Mission tick requested: ${trigger}`,
1143
- sessionId: mission.sessionId || null,
1144
- runId: mission.lastRunId || null,
1145
- data: data || null,
1146
- })
1147
- queueMicrotask(() => {
1148
- void runMissionTick(missionId, trigger).catch((err: unknown) => {
1149
- console.warn(`[missions] mission tick failed for ${missionId}: ${errorMessage(err)}`)
1150
- })
1151
- })
1152
- return mission
1153
- }
1154
-
1155
- export function requestMissionTicksForApprovalDecision(params: {
1156
- approvalId: string
1157
- status: 'approved' | 'rejected'
1158
- sessionId?: string | null
1159
- }): Mission[] {
1160
- ensureMissionControllerRecovered()
1161
- const candidates = listMissions({ status: 'non_terminal' }).filter((mission) => (
1162
- mission.waitState?.kind === 'approval'
1163
- && (
1164
- mission.waitState?.approvalId === params.approvalId
1165
- || (params.sessionId && mission.sessionId === params.sessionId)
1166
- )
1167
- ))
1168
- return candidates
1169
- .map((mission) => requestMissionTick(mission.id, 'approval_resolved', {
1170
- approvalId: params.approvalId,
1171
- status: params.status,
1172
- }))
1173
- .filter((mission): mission is Mission => Boolean(mission))
1174
- }
1175
-
1176
- export function requestMissionTicksForHumanReply(params: {
1177
- sessionId: string
1178
- correlationId?: string | null
1179
- envelopeId?: string | null
1180
- payload?: string | null
1181
- fromSessionId?: string | null
1182
- }): Mission[] {
1183
- ensureMissionControllerRecovered()
1184
- const candidates = listMissions({ sessionId: params.sessionId, status: 'non_terminal' }).filter((mission) => (
1185
- mission.status === 'waiting'
1186
- && mission.waitState?.kind === 'human_reply'
1187
- ))
1188
- return candidates
1189
- .map((mission) => requestMissionTick(mission.id, 'human_reply', {
1190
- correlationId: params.correlationId || null,
1191
- envelopeId: params.envelopeId || null,
1192
- payload: cleanText(params.payload, 320) || null,
1193
- fromSessionId: params.fromSessionId || null,
1194
- }))
1195
- .filter((mission): mission is Mission => Boolean(mission))
1196
- }
1197
-
1198
- export function requestMissionTicksForProviderRecovery(providerKey: string): Mission[] {
1199
- const normalizedProviderKey = cleanText(providerKey, 80)
1200
- if (!normalizedProviderKey) return []
1201
- ensureMissionControllerRecovered()
1202
- const candidates = listMissions({ status: 'non_terminal' }).filter((mission) => (
1203
- mission.waitState?.kind === 'provider'
1204
- && cleanText(mission.waitState?.providerKey, 80) === normalizedProviderKey
1205
- ))
1206
- return candidates
1207
- .map((mission) => requestMissionTick(mission.id, 'provider_recovered', {
1208
- providerKey: normalizedProviderKey,
1209
- }))
1210
- .filter((mission): mission is Mission => Boolean(mission))
1211
- }
1212
-
1213
- export async function runMissionTick(
1214
- missionId: string,
1215
- trigger = 'manual',
1216
- options?: { generateText?: (prompt: string) => Promise<string> },
1217
- ): Promise<Mission | null> {
1218
- ensureMissionControllerRecovered()
1219
- const release = acquireMissionLease(missionId)
1220
- if (!release) return loadMissionById(missionId)
1221
- try {
1222
- let mission = loadMissionById(missionId)
1223
- if (!mission) return null
1224
- if (isMissionTerminal(mission.status)) return mission
1225
- const reconciled = patchMissionStatus(missionId, (current) => reconcileMissionState(current))
1226
- mission = reconciled || mission
1227
- if (mission.status === 'waiting' && !isWaitSatisfied(mission)) return mission
1228
- if (missionHasActiveTask(mission) || missionHasActiveRun(mission) || missionHasActiveChild(mission)) {
1229
- return patchMissionStatus(missionId, (current) => ({
1230
- ...current,
1231
- status: current.status === 'waiting' ? current.status : 'active',
1232
- phase: current.status === 'waiting' ? 'waiting' : 'executing',
1233
- controllerState: {
1234
- ...(current.controllerState || {}),
1235
- tickRequestedAt: now(),
1236
- tickReason: trigger,
1237
- },
1238
- })) || mission
1239
- }
1240
- const planned = applyMissionPlannerPolicies(mission, await planMissionAction(mission, options))
1241
- return await executeMissionPlannerDecision(mission, planned, trigger)
1242
- } finally {
1243
- release()
1244
- }
1245
- }
1246
-
1247
- export function bindMissionToSession(sessionId: string, missionId: string): void {
1248
- patchSession(sessionId, (current) => {
1249
- if (!current) return current
1250
- if (current.missionId === missionId) return current
1251
- return {
1252
- ...current,
1253
- missionId,
1254
- updatedAt: now(),
1255
- }
1256
- })
1257
- }
1258
-
1259
- export function bindMissionToTask(taskId: string, missionId: string): void {
1260
- patchTask(taskId, (current) => {
1261
- if (!current) return current
1262
- if (current.missionId === missionId) return current
1263
- return {
1264
- ...current,
1265
- missionId,
1266
- updatedAt: now(),
1267
- }
1268
- })
1269
- }
1270
-
1271
- function createMission(input: {
1272
- source: MissionSource
1273
- sourceRef?: MissionSourceRef
1274
- objective: string
1275
- successCriteria?: string[]
1276
- currentStep?: string | null
1277
- plannerSummary?: string | null
1278
- sessionId?: string | null
1279
- agentId?: string | null
1280
- projectId?: string | null
1281
- taskId?: string | null
1282
- runId?: string | null
1283
- sourceMessage?: string | null
1284
- parentMissionId?: string | null
1285
- dependencyMissionIds?: string[]
1286
- dependencyTaskIds?: string[]
1287
- }): Mission {
1288
- const timestamp = now()
1289
- const parentMission = input.parentMissionId ? loadMissionById(input.parentMissionId) : null
1290
- const mission = normalizeMissionRecord({
1291
- id: genId(),
1292
- source: input.source,
1293
- sourceRef: input.sourceRef,
1294
- objective: cleanText(input.objective, 300),
1295
- successCriteria: uniqueStrings(input.successCriteria, 6, 180),
1296
- status: 'active',
1297
- phase: 'intake',
1298
- sessionId: input.sessionId || null,
1299
- agentId: input.agentId || null,
1300
- projectId: input.projectId || null,
1301
- rootMissionId: parentMission?.rootMissionId || parentMission?.id || null,
1302
- parentMissionId: input.parentMissionId || null,
1303
- childMissionIds: [],
1304
- dependencyMissionIds: listMissionIds(input.dependencyMissionIds, 128),
1305
- dependencyTaskIds: listMissionIds(input.dependencyTaskIds, 128),
1306
- taskIds: input.taskId ? [input.taskId] : [],
1307
- rootTaskId: input.taskId || null,
1308
- currentStep: cleanText(input.currentStep, 200) || null,
1309
- plannerSummary: cleanText(input.plannerSummary, 320) || null,
1310
- verifierSummary: null,
1311
- blockerSummary: null,
1312
- waitState: null,
1313
- controllerState: {
1314
- tickRequestedAt: timestamp,
1315
- tickReason: 'mission_created',
1316
- attemptCount: 0,
1317
- },
1318
- plannerState: {
1319
- lastDecision: null,
1320
- lastPlannedAt: null,
1321
- planSummary: cleanText(input.plannerSummary, 320) || null,
1322
- },
1323
- verificationState: {
1324
- candidate: false,
1325
- requiredTaskIds: input.taskId ? [input.taskId] : [],
1326
- requiredChildMissionIds: [],
1327
- requiredArtifacts: [],
1328
- evidenceSummary: null,
1329
- lastVerdict: null,
1330
- lastVerifiedAt: null,
1331
- },
1332
- lastRunId: input.runId || null,
1333
- sourceRunId: input.runId || null,
1334
- sourceMessage: cleanText(input.sourceMessage, 600) || null,
1335
- createdAt: timestamp,
1336
- updatedAt: timestamp,
1337
- lastActiveAt: timestamp,
1338
- completedAt: null,
1339
- failedAt: null,
1340
- cancelledAt: null,
1341
- })
1342
- if (!mission.rootMissionId) mission.rootMissionId = mission.parentMissionId || mission.id
1343
- upsertMission(mission.id, mission)
1344
- notify('missions')
1345
- appendMissionEvent({
1346
- missionId: mission.id,
1347
- type: 'created',
1348
- source: input.source,
1349
- summary: `Mission created: ${mission.objective}`,
1350
- sessionId: mission.sessionId || null,
1351
- taskId: input.taskId || null,
1352
- runId: input.runId || null,
1353
- data: {
1354
- successCriteria: mission.successCriteria || [],
1355
- currentStep: mission.currentStep || null,
1356
- plannerSummary: mission.plannerSummary || null,
1357
- sourceRef: mission.sourceRef || null,
1358
- },
1359
- })
1360
- if (mission.parentMissionId) {
1361
- patchMissionStatus(mission.parentMissionId, (parent) => ({
1362
- ...parent,
1363
- childMissionIds: listMissionIds([...(parent.childMissionIds || []), mission.id], 256),
1364
- phase: parent.phase === 'completed' ? 'planning' : parent.phase,
1365
- status: parent.status === 'completed' ? 'active' : parent.status,
1366
- waitState: {
1367
- kind: 'blocked_mission',
1368
- reason: `Waiting on child mission: ${mission.objective}`,
1369
- dependencyMissionId: mission.id,
1370
- },
1371
- dependencyMissionIds: listMissionIds([...(parent.dependencyMissionIds || []), mission.id], 256),
1372
- }))
1373
- appendMissionEvent({
1374
- missionId: mission.parentMissionId,
1375
- type: 'child_created',
1376
- source: input.source,
1377
- summary: `Child mission created: ${mission.objective}`,
1378
- sessionId: mission.sessionId || null,
1379
- runId: input.runId || null,
1380
- data: {
1381
- childMissionId: mission.id,
1382
- objective: mission.objective,
1383
- },
1384
- })
1385
- }
1386
- return mission
1387
- }
1388
-
1389
- export function ensureMissionForTask(
1390
- task: BoardTask,
1391
- options?: {
1392
- source?: MissionSource
1393
- sessionId?: string | null
1394
- runId?: string | null
1395
- },
1396
- ): Mission | null {
1397
- if (!task || !task.id) return null
1398
- const existingMission = loadMissionById(task.missionId)
1399
- if (existingMission) {
1400
- const linked = patchMissionStatus(existingMission.id, (mission) => ensureMissionTaskLink(mission, task.id))
1401
- if (linked) bindMissionToTask(task.id, linked.id)
1402
- if (task.sessionId && linked) bindMissionToSession(task.sessionId, linked.id)
1403
- return linked
1404
- }
1405
-
1406
- const sourceTaskMission = (() => {
1407
- const tasks = loadTasks()
1408
- const sourceTaskId = typeof task.delegatedFromTaskId === 'string' && task.delegatedFromTaskId.trim()
1409
- ? task.delegatedFromTaskId.trim()
1410
- : Array.isArray(task.blockedBy) && task.blockedBy.length > 0
1411
- ? task.blockedBy[0]
1412
- : ''
1413
- if (!sourceTaskId) return null
1414
- return loadMissionById(tasks[sourceTaskId]?.missionId)
1415
- })()
1416
-
1417
- if (sourceTaskMission) {
1418
- const linked = patchMissionStatus(sourceTaskMission.id, (mission) => ensureMissionTaskLink(mission, task.id))
1419
- if (linked) {
1420
- bindMissionToTask(task.id, linked.id)
1421
- if (task.sessionId) bindMissionToSession(task.sessionId, linked.id)
1422
- appendMissionEvent({
1423
- missionId: linked.id,
1424
- type: 'task_linked',
1425
- source: options?.source || missionSourceFromTask(task),
1426
- summary: `Linked task: ${task.title}`,
1427
- sessionId: task.sessionId || null,
1428
- taskId: task.id,
1429
- runId: options?.runId || null,
1430
- data: { taskStatus: task.status },
1431
- })
1432
- }
1433
- return linked
1434
- }
1435
-
1436
- const session = task.sessionId ? loadSession(task.sessionId) : null
1437
- const sessionMission = getMissionForSession(session)
1438
- if (sessionMission && !isMissionTerminal(sessionMission.status)) {
1439
- const linked = patchMissionStatus(sessionMission.id, (mission) => ensureMissionTaskLink(mission, task.id))
1440
- if (linked) {
1441
- bindMissionToTask(task.id, linked.id)
1442
- if (task.sessionId) bindMissionToSession(task.sessionId, linked.id)
1443
- appendMissionEvent({
1444
- missionId: linked.id,
1445
- type: 'task_linked',
1446
- source: options?.source || missionSourceFromTask(task),
1447
- summary: `Linked task: ${task.title}`,
1448
- sessionId: task.sessionId || null,
1449
- taskId: task.id,
1450
- runId: options?.runId || null,
1451
- data: { taskStatus: task.status },
1452
- })
1453
- }
1454
- return linked
1455
- }
1456
-
1457
- const objective = cleanText(task.goalContract?.objective, 300) || cleanText(task.title, 300)
1458
- if (!objective) return null
1459
- const mission = createMission({
1460
- source: options?.source || missionSourceFromTask(task),
1461
- objective,
1462
- successCriteria: task.goalContract?.constraints || [],
1463
- currentStep: cleanText(task.description, 200) || null,
1464
- plannerSummary: task.description || task.title,
1465
- sessionId: options?.sessionId || task.sessionId || null,
1466
- agentId: task.agentId,
1467
- projectId: task.projectId || null,
1468
- taskId: task.id,
1469
- runId: options?.runId || null,
1470
- sourceMessage: task.description || task.title,
1471
- })
1472
- bindMissionToTask(task.id, mission.id)
1473
- if (task.sessionId) bindMissionToSession(task.sessionId, mission.id)
1474
- appendMissionEvent({
1475
- missionId: mission.id,
1476
- type: 'task_linked',
1477
- source: options?.source || missionSourceFromTask(task),
1478
- summary: `Linked task: ${task.title}`,
1479
- sessionId: task.sessionId || null,
1480
- taskId: task.id,
1481
- runId: options?.runId || null,
1482
- data: { taskStatus: task.status },
1483
- })
1484
- return loadMissionById(mission.id)
1485
- }
1486
-
1487
- function applyTurnDecisionToMission(
1488
- decision: MissionTurnDecision,
1489
- params: {
1490
- session: Session
1491
- source: MissionSource
1492
- runId?: string | null
1493
- message: string
1494
- currentMission: Mission | null
1495
- },
1496
- ): Mission | null {
1497
- if (decision.action === 'none') return null
1498
- if (decision.action === 'attach_current' && params.currentMission) {
1499
- const updated = patchMissionStatus(params.currentMission.id, (mission) => ({
1500
- ...mission,
1501
- phase: mission.status === 'waiting' ? 'waiting' : mission.phase,
1502
- currentStep: decision.currentStep || mission.currentStep || null,
1503
- plannerSummary: decision.plannerSummary || mission.plannerSummary || null,
1504
- lastRunId: params.runId || mission.lastRunId || null,
1505
- }))
1506
- if (updated) {
1507
- bindMissionToSession(params.session.id, updated.id)
1508
- appendMissionEvent({
1509
- missionId: updated.id,
1510
- type: 'attached',
1511
- source: params.source,
1512
- summary: `Attached turn to mission: ${updated.objective}`,
1513
- sessionId: params.session.id,
1514
- runId: params.runId || null,
1515
- data: { message: cleanText(params.message, 320) },
1516
- })
1517
- }
1518
- return updated
1519
- }
1520
- if (decision.action !== 'create_new') return null
1521
- const mission = createMission({
1522
- source: params.source,
1523
- objective: decision.objective,
1524
- successCriteria: decision.successCriteria,
1525
- currentStep: decision.currentStep || null,
1526
- plannerSummary: decision.plannerSummary || null,
1527
- sessionId: params.session.id,
1528
- agentId: params.session.agentId || null,
1529
- projectId: params.session.projectId || null,
1530
- runId: params.runId || null,
1531
- sourceMessage: params.message,
1532
- })
1533
- bindMissionToSession(params.session.id, mission.id)
1534
- return loadMissionById(mission.id)
1535
- }
1536
-
1537
- export async function resolveMissionForTurn(params: {
1538
- session: Session
1539
- message: string
1540
- source: string
1541
- internal: boolean
1542
- runId?: string | null
1543
- explicitMissionId?: string | null
1544
- generateText?: (prompt: string) => Promise<string>
1545
- }): Promise<Mission | null> {
1546
- const explicitMission = loadMissionById(params.explicitMissionId)
1547
- if (explicitMission) {
1548
- bindMissionToSession(params.session.id, explicitMission.id)
1549
- return explicitMission
1550
- }
1551
-
1552
- const currentMission = getMissionForSession(params.session)
1553
- if (params.source === 'task' && currentMission) {
1554
- bindMissionToSession(params.session.id, currentMission.id)
1555
- return currentMission
1556
- }
1557
- if (params.internal) {
1558
- if (currentMission) bindMissionToSession(params.session.id, currentMission.id)
1559
- return currentMission
1560
- }
1561
-
1562
- let decision: MissionTurnDecision | null = null
1563
- try {
1564
- decision = await classifyMissionTurn({
1565
- sessionId: params.session.id,
1566
- agentId: params.session.agentId || null,
1567
- message: params.message,
1568
- recentMessages: Array.isArray(params.session.messages) ? params.session.messages : [],
1569
- currentMission: currentMission ? buildMissionSummary(currentMission) : null,
1570
- session: params.session,
1571
- }, params.generateText ? { generateText: params.generateText } : undefined)
1572
- } catch (err: unknown) {
1573
- console.warn(`[missions] resolveMissionForTurn failed for ${params.session.id}: ${errorMessage(err)}`)
1574
- return null
1575
- }
1576
-
1577
- if (!decision) return null
1578
- return applyTurnDecisionToMission(decision, {
1579
- session: params.session,
1580
- source: params.source === 'chat' ? 'chat' : 'connector',
1581
- runId: params.runId || null,
1582
- message: params.message,
1583
- currentMission,
1584
- })
1585
- }
1586
-
1587
- function missionPhaseForVerdict(decision: MissionOutcomeDecision, mission: Mission): MissionPhase {
1588
- if (decision.phase) return decision.phase
1589
- if (decision.verdict === 'completed') return 'completed'
1590
- if (decision.verdict === 'failed') return 'failed'
1591
- if (decision.verdict === 'waiting') return 'waiting'
1592
- if (decision.verdict === 'replan') return 'planning'
1593
- if (mission.phase === 'planning') return 'executing'
1594
- return 'verifying'
1595
- }
1596
-
1597
- function applyMissionOutcomePolicies(
1598
- mission: Mission,
1599
- decision: MissionOutcomeDecision,
1600
- ): MissionOutcomeDecision {
1601
- if (decision.verdict !== 'waiting' || !shouldSuppressMissionHumanLoopWait(decision.waitKind)) return decision
1602
- const currentStep = decision.currentStep || mission.currentStep
1603
- if (hasTerminalMissionEvidence(mission) || ((mission.taskIds?.length || 0) === 0 && (mission.childMissionIds?.length || 0) === 0)) {
1604
- return {
1605
- verdict: 'completed',
1606
- confidence: decision.confidence,
1607
- phase: 'completed',
1608
- ...(currentStep ? { currentStep } : {}),
1609
- verifierSummary: 'Mission human-loop waits are disabled, so the completed work was closed instead of waiting for another reply.',
1610
- }
1611
- }
1612
- return {
1613
- verdict: 'replan',
1614
- confidence: decision.confidence,
1615
- phase: 'planning',
1616
- ...(currentStep ? { currentStep } : {}),
1617
- verifierSummary: 'Mission human-loop waits are disabled, so the controller kept the mission active instead of waiting for another reply.',
1618
- }
1619
- }
1620
-
1621
- function summaryForOutcome(decision: MissionOutcomeDecision, fallback: string): string {
1622
- return cleanText(decision.verifierSummary, 360) || cleanText(fallback, 360) || 'Mission updated.'
1623
- }
1624
-
1625
- export async function applyMissionOutcomeForTurn(params: {
1626
- session: Session
1627
- missionId: string
1628
- source: string
1629
- runId?: string | null
1630
- message: string
1631
- assistantText?: string | null
1632
- error?: string | null
1633
- toolEvents?: MessageToolEvent[]
1634
- generateText?: (prompt: string) => Promise<string>
1635
- }): Promise<Mission | null> {
1636
- const mission = loadMissionById(params.missionId)
1637
- if (!mission) return null
1638
- const taskSummaries = listTaskSummaries(mission.taskIds)
1639
- let decision: MissionOutcomeDecision | null = null
1640
- try {
1641
- decision = await verifyMissionOutcome({
1642
- sessionId: params.session.id,
1643
- agentId: params.session.agentId || null,
1644
- userMessage: params.message,
1645
- assistantText: params.assistantText || null,
1646
- error: params.error || null,
1647
- toolEvents: params.toolEvents,
1648
- currentMission: buildMissionSummary(mission),
1649
- linkedTaskSummaries: taskSummaries,
1650
- }, params.generateText ? { generateText: params.generateText } : undefined)
1651
- } catch (err: unknown) {
1652
- console.warn(`[missions] applyMissionOutcomeForTurn failed for ${params.session.id}: ${errorMessage(err)}`)
1653
- return mission
1654
- }
1655
- if (!decision) return mission
1656
- decision = applyMissionOutcomePolicies(mission, decision)
1657
-
1658
- const fallbackSummary = params.error
1659
- ? `Run ended with error: ${params.error}`
1660
- : cleanText(params.assistantText, 360) || 'Mission run completed.'
1661
- const outcomeSummary = summaryForOutcome(decision, fallbackSummary)
1662
- const updated = patchMissionStatus(mission.id, (current) => {
1663
- const next: Mission = {
1664
- ...current,
1665
- phase: missionPhaseForVerdict(decision, current),
1666
- currentStep: decision.currentStep || current.currentStep || null,
1667
- verifierSummary: outcomeSummary,
1668
- lastRunId: params.runId || current.lastRunId || null,
1669
- waitState: null,
1670
- blockerSummary: null,
1671
- completedAt: current.completedAt || null,
1672
- failedAt: current.failedAt || null,
1673
- cancelledAt: current.cancelledAt || null,
1674
- }
1675
- if (decision.verdict === 'completed') {
1676
- next.status = 'completed'
1677
- next.phase = 'completed'
1678
- next.waitState = null
1679
- next.completedAt = now()
1680
- } else if (decision.verdict === 'failed') {
1681
- next.status = 'failed'
1682
- next.phase = 'failed'
1683
- next.failedAt = now()
1684
- next.blockerSummary = outcomeSummary
1685
- } else if (decision.verdict === 'waiting') {
1686
- next.status = 'waiting'
1687
- next.phase = 'waiting'
1688
- next.waitState = {
1689
- kind: decision.waitKind || 'other',
1690
- reason: cleanText(decision.waitReason, 220) || outcomeSummary,
1691
- }
1692
- } else if (decision.verdict === 'replan') {
1693
- next.status = 'active'
1694
- next.phase = 'planning'
1695
- next.waitState = null
1696
- next.blockerSummary = null
1697
- } else {
1698
- next.status = 'active'
1699
- if (next.phase === 'completed' || next.phase === 'failed' || next.phase === 'waiting') {
1700
- next.phase = 'executing'
1701
- }
1702
- }
1703
- return next
1704
- })
1705
- if (!updated) return mission
1706
-
1707
- logActivity({
1708
- entityType: 'mission',
1709
- entityId: updated.id,
1710
- action: `phase_${updated.phase}`,
1711
- actor: 'system',
1712
- summary: `Mission "${updated.objective?.slice(0, 60) || updated.id}" → ${updated.phase} (${decision.verdict})`,
1713
- })
1714
-
1715
- appendMissionEvent({
1716
- missionId: updated.id,
1717
- type: 'run_result',
1718
- source: params.source === 'heartbeat' || params.source === 'main-loop-followup'
1719
- ? (params.source as MissionSource)
1720
- : 'chat',
1721
- summary: outcomeSummary,
1722
- sessionId: params.session.id,
1723
- runId: params.runId || null,
1724
- data: {
1725
- verdict: decision.verdict,
1726
- phase: updated.phase,
1727
- status: updated.status,
1728
- currentStep: updated.currentStep || null,
1729
- waitState: updated.waitState || null,
1730
- },
1731
- })
1732
-
1733
- if (decision.verdict === 'waiting') {
1734
- appendMissionEvent({
1735
- missionId: updated.id,
1736
- type: 'waiting',
1737
- source: params.source === 'heartbeat' || params.source === 'main-loop-followup'
1738
- ? (params.source as MissionSource)
1739
- : 'chat',
1740
- summary: updated.waitState?.reason || outcomeSummary,
1741
- sessionId: params.session.id,
1742
- runId: params.runId || null,
1743
- data: updated.waitState ? updated.waitState as unknown as Record<string, unknown> : null,
1744
- })
1745
- } else if (decision.verdict === 'completed') {
1746
- appendMissionEvent({
1747
- missionId: updated.id,
1748
- type: 'completed',
1749
- source: params.source === 'heartbeat' || params.source === 'main-loop-followup'
1750
- ? (params.source as MissionSource)
1751
- : 'chat',
1752
- summary: outcomeSummary,
1753
- sessionId: params.session.id,
1754
- runId: params.runId || null,
1755
- data: { status: updated.status },
1756
- })
1757
- } else if (decision.verdict === 'failed') {
1758
- appendMissionEvent({
1759
- missionId: updated.id,
1760
- type: 'failed',
1761
- source: params.source === 'heartbeat' || params.source === 'main-loop-followup'
1762
- ? (params.source as MissionSource)
1763
- : 'chat',
1764
- summary: outcomeSummary,
1765
- sessionId: params.session.id,
1766
- runId: params.runId || null,
1767
- data: { status: updated.status },
1768
- })
1769
- }
1770
-
1771
- bindMissionToSession(params.session.id, updated.id)
1772
- if (
1773
- params.source !== 'chat'
1774
- && updated.status === 'active'
1775
- && updated.phase !== 'executing'
1776
- && updated.phase !== 'dispatching'
1777
- && !missionHasActiveTask(updated)
1778
- && !missionHasActiveRun(updated)
1779
- && !missionHasActiveChild(updated)
1780
- ) {
1781
- requestMissionTick(updated.id, 'run_outcome', {
1782
- source: params.source,
1783
- verdict: decision.verdict,
1784
- runId: params.runId || null,
1785
- })
1786
- }
1787
- if (updated.parentMissionId && isMissionTerminal(updated.status)) {
1788
- noteParentMissionChildOutcome(updated)
1789
- }
1790
- return updated
1791
- }
1792
-
1793
- function noteParentMissionChildOutcome(childMission: Mission): void {
1794
- if (!childMission.parentMissionId) return
1795
- const parent = loadMissionById(childMission.parentMissionId)
1796
- if (!parent) return
1797
- const summary = childMission.status === 'completed'
1798
- ? `Child mission completed: ${childMission.objective}`
1799
- : childMission.status === 'failed'
1800
- ? `Child mission failed: ${childMission.objective}`
1801
- : `Child mission updated: ${childMission.objective}`
1802
- appendMissionEvent({
1803
- missionId: parent.id,
1804
- type: childMission.status === 'completed' ? 'child_completed' : childMission.status === 'failed' ? 'child_failed' : 'status_change',
1805
- source: childMission.source,
1806
- summary,
1807
- sessionId: parent.sessionId || null,
1808
- runId: childMission.lastRunId || null,
1809
- data: {
1810
- childMissionId: childMission.id,
1811
- childStatus: childMission.status,
1812
- childPhase: childMission.phase,
1813
- },
1814
- })
1815
- requestMissionTick(parent.id, 'child_mission_changed', {
1816
- childMissionId: childMission.id,
1817
- childStatus: childMission.status,
1818
- })
1819
- wakeDependentMissions(childMission.id, 'mission')
1820
- }
1821
-
1822
- function wakeDependentMissions(completedId: string, kind: 'mission' | 'task'): void {
1823
- const allMissions = Object.values(loadMissions()).map(normalizeMissionRecord)
1824
- for (const candidate of allMissions) {
1825
- if (isMissionTerminal(candidate.status)) continue
1826
- const deps = kind === 'mission'
1827
- ? Array.isArray(candidate.dependencyMissionIds) ? candidate.dependencyMissionIds : []
1828
- : Array.isArray(candidate.dependencyTaskIds) ? candidate.dependencyTaskIds : []
1829
- if (deps.includes(completedId)) {
1830
- requestMissionTick(candidate.id, `dependency_${kind}_completed`, { [`completed${kind === 'mission' ? 'Mission' : 'Task'}Id`]: completedId })
1831
- }
1832
- }
1833
- }
1834
-
1835
- export function performMissionAction(params: {
1836
- missionId: string
1837
- action: 'resume' | 'replan' | 'cancel' | 'retry_verification' | 'wait'
1838
- reason?: string | null
1839
- waitKind?: NonNullable<Mission['waitState']>['kind']
1840
- untilAt?: number | null
1841
- }): { mission: Mission; event: MissionEvent } | null {
1842
- const mission = loadMissionById(params.missionId)
1843
- if (!mission) return null
1844
- const summaryReason = cleanText(params.reason, 220) || null
1845
- const updated = patchMissionStatus(mission.id, (current) => {
1846
- if (params.action === 'cancel') {
1847
- return {
1848
- ...current,
1849
- status: 'cancelled',
1850
- phase: 'failed',
1851
- blockerSummary: summaryReason || 'Mission cancelled by operator.',
1852
- waitState: null,
1853
- cancelledAt: now(),
1854
- }
1855
- }
1856
- if (params.action === 'wait') {
1857
- return {
1858
- ...current,
1859
- status: 'waiting',
1860
- phase: 'waiting',
1861
- waitState: {
1862
- kind: params.waitKind || 'other',
1863
- reason: summaryReason || 'Mission paused by operator.',
1864
- untilAt: typeof params.untilAt === 'number' ? params.untilAt : null,
1865
- },
1866
- }
1867
- }
1868
- if (params.action === 'retry_verification') {
1869
- return {
1870
- ...current,
1871
- status: 'active',
1872
- phase: 'verifying',
1873
- waitState: null,
1874
- blockerSummary: null,
1875
- verificationState: {
1876
- ...(current.verificationState || { candidate: false }),
1877
- candidate: true,
1878
- },
1879
- }
1880
- }
1881
- return {
1882
- ...current,
1883
- status: 'active',
1884
- phase: 'planning',
1885
- waitState: null,
1886
- blockerSummary: null,
1887
- controllerState: {
1888
- ...(current.controllerState || {}),
1889
- tickRequestedAt: now(),
1890
- tickReason: params.action,
1891
- },
1892
- }
1893
- })
1894
- if (!updated) return null
1895
- const event = appendMissionEvent({
1896
- missionId: updated.id,
1897
- type: 'operator_action',
1898
- source: 'system',
1899
- summary: `${params.action.replace(/_/g, ' ')} mission`,
1900
- sessionId: updated.sessionId || null,
1901
- runId: updated.lastRunId || null,
1902
- data: {
1903
- action: params.action,
1904
- reason: summaryReason,
1905
- waitKind: params.waitKind || null,
1906
- untilAt: typeof params.untilAt === 'number' ? params.untilAt : null,
1907
- },
1908
- })
1909
- if (params.action !== 'wait' && params.action !== 'cancel') {
1910
- requestMissionTick(updated.id, `operator:${params.action}`, {
1911
- reason: summaryReason,
1912
- })
1913
- }
1914
- return { mission: updated, event }
1915
- }
1916
-
1917
- export function ensureMissionForSchedule(
1918
- schedule: Schedule,
1919
- options?: {
1920
- sessionId?: string | null
1921
- runId?: string | null
1922
- },
1923
- ): Mission | null {
1924
- if (!schedule?.id) return null
1925
- const linked = loadMissionById(schedule.linkedMissionId)
1926
- if (linked) return linked
1927
- const objective = cleanText(schedule.taskPrompt, 300)
1928
- || cleanText(schedule.message, 300)
1929
- || cleanText(schedule.name, 300)
1930
- if (!objective) return null
1931
- const mission = createMission({
1932
- source: 'schedule',
1933
- sourceRef: {
1934
- kind: 'schedule',
1935
- scheduleId: schedule.id,
1936
- recurring: schedule.scheduleType !== 'once',
1937
- },
1938
- objective,
1939
- currentStep: cleanText(schedule.taskPrompt || schedule.message || schedule.name, 200) || null,
1940
- plannerSummary: schedule.taskPrompt || schedule.message || schedule.name,
1941
- sessionId: options?.sessionId || schedule.createdInSessionId || null,
1942
- agentId: schedule.agentId,
1943
- projectId: schedule.projectId || null,
1944
- runId: options?.runId || null,
1945
- sourceMessage: schedule.taskPrompt || schedule.message || schedule.name,
1946
- })
1947
- schedule.linkedMissionId = mission.id
1948
- upsertSchedule(schedule.id, {
1949
- ...schedule,
1950
- linkedMissionId: mission.id,
1951
- })
1952
- return mission
1953
- }
1954
-
1955
- export function noteScheduleMissionTriggered(
1956
- schedule: Schedule,
1957
- options?: {
1958
- runId?: string | null
1959
- taskId?: string | null
1960
- wakeOnly?: boolean
1961
- sessionId?: string | null
1962
- },
1963
- ): Mission | null {
1964
- const mission = ensureMissionForSchedule(schedule, {
1965
- sessionId: options?.sessionId || schedule.createdInSessionId || null,
1966
- runId: options?.runId || null,
1967
- })
1968
- if (!mission) return null
1969
- const updated = patchMissionStatus(mission.id, (current) => ({
1970
- ...current,
1971
- status: 'active',
1972
- phase: options?.wakeOnly ? 'planning' : 'dispatching',
1973
- currentStep: cleanText(schedule.taskPrompt || schedule.message || schedule.name, 200) || current.currentStep || null,
1974
- controllerState: {
1975
- ...(current.controllerState || {}),
1976
- tickRequestedAt: now(),
1977
- tickReason: options?.wakeOnly ? 'schedule_wake' : 'schedule_task',
1978
- currentTaskId: options?.taskId || current.controllerState?.currentTaskId || null,
1979
- },
1980
- }))
1981
- const sessionId = options?.sessionId || schedule.createdInSessionId || null
1982
- if (updated && sessionId) bindMissionToSession(sessionId, updated.id)
1983
- if (updated) {
1984
- appendMissionEvent({
1985
- missionId: updated.id,
1986
- type: 'source_triggered',
1987
- source: 'schedule',
1988
- summary: options?.wakeOnly
1989
- ? `Schedule wake fired: ${schedule.name}`
1990
- : `Schedule task fired: ${schedule.name}`,
1991
- sessionId,
1992
- runId: options?.runId || null,
1993
- taskId: options?.taskId || null,
1994
- data: {
1995
- scheduleId: schedule.id,
1996
- wakeOnly: options?.wakeOnly === true,
1997
- },
1998
- })
1999
- }
2000
- return updated
2001
- }
2002
-
2003
- export function ensureDelegationMission(input: {
2004
- task: string
2005
- backend?: DelegationJobRecord['backend']
2006
- parentSessionId?: string | null
2007
- childSessionId?: string | null
2008
- agentId?: string | null
2009
- parentMissionId?: string | null
2010
- jobId?: string | null
2011
- }): Mission | null {
2012
- const explicitParent = loadMissionById(input.parentMissionId)
2013
- const sessionParent = input.parentSessionId ? getMissionForSession(loadSession(input.parentSessionId)) : null
2014
- const parentMission = explicitParent || sessionParent
2015
- if (!parentMission) return null
2016
- const childSession = input.childSessionId ? loadSession(input.childSessionId) : null
2017
- const existing = childSession?.missionId ? loadMissionById(childSession.missionId) : null
2018
- if (existing && existing.parentMissionId === parentMission.id) return existing
2019
- const childMission = createMission({
2020
- source: 'delegation',
2021
- sourceRef: {
2022
- kind: 'delegation',
2023
- parentMissionId: parentMission.id,
2024
- backend: input.backend === 'codex' || input.backend === 'claude' || input.backend === 'opencode' || input.backend === 'gemini'
2025
- ? input.backend
2026
- : 'agent',
2027
- },
2028
- objective: cleanText(input.task, 300) || 'Delegated work',
2029
- currentStep: cleanText(input.task, 200) || 'Execute delegated task',
2030
- plannerSummary: cleanText(input.task, 320) || 'Execute delegated task',
2031
- sessionId: input.childSessionId || input.parentSessionId || null,
2032
- agentId: input.agentId || null,
2033
- projectId: parentMission.projectId || null,
2034
- sourceMessage: cleanText(input.task, 600) || null,
2035
- parentMissionId: parentMission.id,
2036
- })
2037
- if (input.childSessionId) bindMissionToSession(input.childSessionId, childMission.id)
2038
- return childMission
2039
- }
2040
-
2041
- export function syncDelegationMissionFromJob(jobId: string): Mission | null {
2042
- const job = (loadDelegationJobs() as Record<string, DelegationJobRecord>)[jobId]
2043
- if (!job) return null
2044
- const mission = loadMissionById(job.missionId) || ensureDelegationMission({
2045
- task: job.task,
2046
- backend: job.backend,
2047
- parentSessionId: job.parentSessionId || null,
2048
- childSessionId: job.childSessionId || null,
2049
- agentId: job.agentId || null,
2050
- parentMissionId: job.parentMissionId || null,
2051
- jobId,
2052
- })
2053
- if (!mission) return null
2054
- const status = job.status
2055
- const updated = patchMissionStatus(mission.id, (current) => {
2056
- if (status === 'queued' || status === 'running') {
2057
- return {
2058
- ...current,
2059
- status: 'active',
2060
- phase: 'executing',
2061
- currentStep: cleanText(job.task, 200) || current.currentStep || null,
2062
- }
2063
- }
2064
- if (status === 'completed') {
2065
- return {
2066
- ...current,
2067
- status: 'completed',
2068
- phase: 'completed',
2069
- verifierSummary: cleanText(job.result || job.resultPreview, 320) || current.verifierSummary || null,
2070
- completedAt: now(),
2071
- }
2072
- }
2073
- if (status === 'failed') {
2074
- return {
2075
- ...current,
2076
- status: 'failed',
2077
- phase: 'failed',
2078
- blockerSummary: cleanText(job.error, 240) || 'Delegation failed.',
2079
- failedAt: now(),
2080
- }
2081
- }
2082
- return {
2083
- ...current,
2084
- status: 'cancelled',
2085
- phase: 'failed',
2086
- cancelledAt: now(),
2087
- }
2088
- })
2089
- if (updated && updated.parentMissionId && isMissionTerminal(updated.status)) noteParentMissionChildOutcome(updated)
2090
- return updated
2091
- }
2092
-
2093
- export function noteMissionTaskStarted(task: BoardTask, runId?: string | null): Mission | null {
2094
- const mission = ensureMissionForTask(task, {
2095
- source: missionSourceFromTask(task),
2096
- runId: runId || null,
2097
- })
2098
- if (!mission) return null
2099
- const updated = patchMissionStatus(mission.id, (current) => ({
2100
- ...ensureMissionTaskLink(current, task.id),
2101
- status: 'active',
2102
- phase: 'executing',
2103
- currentStep: cleanText(task.title, 200) || current.currentStep || null,
2104
- controllerState: {
2105
- ...(current.controllerState || {}),
2106
- activeRunId: runId || current.controllerState?.activeRunId || null,
2107
- currentTaskId: task.id,
2108
- tickRequestedAt: now(),
2109
- tickReason: 'task_started',
2110
- },
2111
- }))
2112
- if (updated) {
2113
- appendMissionEvent({
2114
- missionId: updated.id,
2115
- type: 'task_started',
2116
- source: missionSourceFromTask(task),
2117
- summary: `Task started: ${task.title}`,
2118
- sessionId: task.sessionId || null,
2119
- taskId: task.id,
2120
- runId: runId || null,
2121
- data: { taskStatus: task.status },
2122
- })
2123
- }
2124
- return updated
2125
- }
2126
-
2127
- export function noteMissionTaskFinished(task: BoardTask, status: 'completed' | 'failed' | 'cancelled', runId?: string | null): Mission | null {
2128
- const mission = loadMissionById(task.missionId) || ensureMissionForTask(task, {
2129
- source: missionSourceFromTask(task),
2130
- runId: runId || null,
2131
- })
2132
- if (!mission) return null
2133
- const summary = status === 'completed'
2134
- ? `Task completed: ${task.title}`
2135
- : status === 'cancelled'
2136
- ? `Task cancelled: ${task.title}`
2137
- : `Task failed: ${task.title}`
2138
- const updated = patchMissionStatus(mission.id, (current) => {
2139
- const linked = ensureMissionTaskLink(current, task.id)
2140
- const taskSummaries = listTaskSummaries(linked.taskIds)
2141
- const hasOpenTask = taskSummaries.some((row) => !['completed', 'failed', 'cancelled', 'archived'].includes(row.status))
2142
- const hasFailedTask = taskSummaries.some((row) => row.status === 'failed')
2143
- const allCancelled = taskSummaries.length > 0 && taskSummaries.every((row) => row.status === 'cancelled')
2144
- const completedAt = !hasOpenTask && !hasFailedTask && status === 'completed'
2145
- ? now()
2146
- : current.completedAt || null
2147
- const cancelledAt = allCancelled ? now() : current.cancelledAt || null
2148
- return {
2149
- ...linked,
2150
- status: hasFailedTask
2151
- ? 'waiting'
2152
- : allCancelled
2153
- ? 'cancelled'
2154
- : hasOpenTask
2155
- ? 'active'
2156
- : 'completed',
2157
- phase: hasFailedTask
2158
- ? 'waiting'
2159
- : allCancelled
2160
- ? 'failed'
2161
- : hasOpenTask
2162
- ? 'planning'
2163
- : 'completed',
2164
- blockerSummary: status === 'failed' ? cleanText(task.error, 240) || summary : current.blockerSummary || null,
2165
- waitState: status === 'failed'
2166
- ? {
2167
- kind: 'blocked_task',
2168
- reason: cleanText(task.error, 220) || summary,
2169
- dependencyTaskId: task.id,
2170
- }
2171
- : null,
2172
- controllerState: {
2173
- ...(current.controllerState || {}),
2174
- activeRunId: null,
2175
- currentTaskId: hasOpenTask ? current.controllerState?.currentTaskId || null : null,
2176
- tickRequestedAt: now(),
2177
- tickReason: status === 'completed' ? 'task_completed' : status === 'failed' ? 'task_failed' : 'task_cancelled',
2178
- },
2179
- completedAt,
2180
- cancelledAt,
2181
- failedAt: status === 'failed' ? now() : current.failedAt || null,
2182
- }
2183
- })
2184
- if (updated) {
2185
- appendMissionEvent({
2186
- missionId: updated.id,
2187
- type: status === 'completed' ? 'task_completed' : 'task_failed',
2188
- source: missionSourceFromTask(task),
2189
- summary,
2190
- sessionId: task.sessionId || null,
2191
- taskId: task.id,
2192
- runId: runId || null,
2193
- data: {
2194
- taskStatus: status,
2195
- result: cleanText(task.result, 280) || null,
2196
- error: cleanText(task.error, 220) || null,
2197
- },
2198
- })
2199
- }
2200
- if (updated && !isMissionTerminal(updated.status)) {
2201
- requestMissionTick(updated.id, status === 'completed' ? 'task_state_changed' : 'task_blocked', {
2202
- taskId: task.id,
2203
- taskStatus: status,
2204
- })
2205
- }
2206
- wakeDependentMissions(task.id, 'task')
2207
- return updated
2208
- }
2209
-
2210
- export function buildMissionContextBlock(mission: Mission | null | undefined): string {
2211
- if (!mission) return ''
2212
- const summary = buildMissionSummary(mission)
2213
- const linkedTasks = listTaskSummaries(summary.taskIds)
2214
- const childMissions = listChildMissions(mission.id, 4)
2215
- const taskBlock = linkedTasks.length > 0
2216
- ? linkedTasks
2217
- .slice(0, 6)
2218
- .map((task) => {
2219
- const base = `- [${task.status}] ${task.title}`
2220
- if (task.status === 'completed' && task.result) {
2221
- return `${base}: ${task.result.slice(0, 120)}`
2222
- }
2223
- return base
2224
- })
2225
- .join('\n')
2226
- : ''
2227
- const childBlock = childMissions.length > 0
2228
- ? childMissions.map((child) => `- [${child.status}/${child.phase}] ${child.objective}`).join('\n')
2229
- : ''
2230
- return [
2231
- '## Active Mission',
2232
- `Objective: ${summary.objective}`,
2233
- mission.successCriteria?.length ? `Success criteria: ${mission.successCriteria.join(' | ')}` : '',
2234
- `Status: ${summary.status}`,
2235
- `Phase: ${summary.phase}`,
2236
- mission.sourceRef ? `Source: ${mission.sourceRef.kind}` : '',
2237
- summary.currentStep ? `Current step: ${summary.currentStep}` : '',
2238
- summary.waitingReason ? `Waiting reason: ${summary.waitingReason}` : '',
2239
- mission.plannerSummary ? `Planner summary: ${mission.plannerSummary}` : '',
2240
- mission.verifierSummary ? `Verifier summary: ${mission.verifierSummary}` : '',
2241
- mission.verificationState?.candidate ? 'Verification candidate: true' : '',
2242
- taskBlock ? `Linked tasks:\n${taskBlock}` : '',
2243
- childBlock ? `Child missions:\n${childBlock}` : '',
2244
- 'Advance the mission. Do not confuse planning, promises, or partial progress with completion.',
2245
- ].filter(Boolean).join('\n')
2246
- }
2247
-
2248
- export function buildMissionHeartbeatPrompt(session: Session, fallbackPrompt: string): string | null {
2249
- const mission = getMissionForSession(session)
2250
- if (!mission || isMissionTerminal(mission.status)) return null
2251
- const contextBlock = buildMissionContextBlock(mission)
2252
- return [
2253
- 'MAIN_AGENT_HEARTBEAT_TICK',
2254
- `Time: ${new Date().toISOString()}`,
2255
- contextBlock,
2256
- fallbackPrompt ? `Base heartbeat instructions:\n${fallbackPrompt}` : '',
2257
- '',
2258
- 'You are checking the durable mission state for this agent.',
2259
- 'Take the single highest-value next step for the mission.',
2260
- 'If the mission is genuinely waiting on an external dependency, say so plainly.',
2261
- 'Reply HEARTBEAT_OK only when the mission is completed or waiting and no immediate action should be taken.',
2262
- ].filter(Boolean).join('\n')
2263
- }
1
+ export * from './mission-service/queries'
2
+ export * from './mission-service/recovery'
3
+ export * from './mission-service/ticks'
4
+ export * from './mission-service/bindings'
5
+ export * from './mission-service/actions'
6
+ export * from './mission-service/context'