@swarmclawai/swarmclaw 1.2.1 → 1.2.3

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