@swarmclawai/swarmclaw 1.2.8 → 1.2.9

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