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