@bbigbang/core 0.1.0
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/dist/config.js +380 -0
- package/dist/execution/executionDispatcher.js +3810 -0
- package/dist/main.js +90 -0
- package/dist/nodeEventHistory.js +206 -0
- package/dist/scheduler/dreamLogic.js +50 -0
- package/dist/scheduler/dreamScheduler.js +65 -0
- package/dist/services/agentFileAccessService.js +1913 -0
- package/dist/services/agentRuntimeCleanupBroker.js +62 -0
- package/dist/services/agentSkillsBroker.js +118 -0
- package/dist/services/agentSkillsService.js +83 -0
- package/dist/services/agentWorkspaceBroker.js +937 -0
- package/dist/services/agentWorkspaceService.js +70 -0
- package/dist/services/appVersion.js +14 -0
- package/dist/services/auth.js +586 -0
- package/dist/services/claudeControlBroker.js +154 -0
- package/dist/services/claudeTranscriptBroker.js +100 -0
- package/dist/services/claudeTranscriptService.js +359 -0
- package/dist/services/codexAppServerBroker.js +155 -0
- package/dist/services/codexTranscriptBroker.js +98 -0
- package/dist/services/codexTranscriptService.js +961 -0
- package/dist/services/droidMissionBroker.js +124 -0
- package/dist/services/droidMissionImporter.js +630 -0
- package/dist/services/droidModelOptions.js +165 -0
- package/dist/services/hubServerRegistrationService.js +268 -0
- package/dist/services/libraryManifest.js +43 -0
- package/dist/services/libraryScaffold.js +26 -0
- package/dist/services/libraryService.js +2263 -0
- package/dist/services/memoryService.js +386 -0
- package/dist/services/missionEvidence.js +377 -0
- package/dist/services/missionService.js +2361 -0
- package/dist/services/missionTrace.js +158 -0
- package/dist/services/nativeMissionBriefParser.js +120 -0
- package/dist/services/nativeMissionOrchestrator.js +2045 -0
- package/dist/services/nativeMissionReportGenerator.js +227 -0
- package/dist/services/nativeMissionValidationRunner.js +452 -0
- package/dist/services/nativeMissionWorkerBroker.js +190 -0
- package/dist/services/nodeRegistry.js +34 -0
- package/dist/services/nodeStateReconciler.js +97 -0
- package/dist/services/panelMediaScanner.js +119 -0
- package/dist/services/persistentRuntimeJsonlClient.js +153 -0
- package/dist/services/platformAgentPolicy.js +180 -0
- package/dist/services/platformAgentService.js +2041 -0
- package/dist/services/projectAccessResolver.js +93 -0
- package/dist/services/projectService.js +392 -0
- package/dist/services/resourceSpaceService.js +140 -0
- package/dist/services/scenarioRuntimeService.js +1130 -0
- package/dist/services/suggestedPlannerService.js +868 -0
- package/dist/services/workbenchGitBroker.js +161 -0
- package/dist/services/workbenchGitService.js +69 -0
- package/dist/services/workbenchInspectBroker.js +65 -0
- package/dist/services/workbenchNodePathService.js +79 -0
- package/dist/services/workbenchRegistryService.js +240 -0
- package/dist/services/workbenchRootService.js +181 -0
- package/dist/services/workbenchTerminalBroker.js +378 -0
- package/dist/services/workspaceRunOwnership.js +60 -0
- package/dist/services/workspaceScaffold.js +105 -0
- package/dist/services/workspaceSessionRuntimeService.js +576 -0
- package/dist/services/workspaceSessionService.js +245 -0
- package/dist/services/workspaceToolActionRunner.js +1582 -0
- package/dist/services/workspaceToolErrors.js +10 -0
- package/dist/services/workspaceToolExecutionUtils.js +895 -0
- package/dist/services/workspaceToolLatestStateProjector.js +91 -0
- package/dist/services/workspaceToolManifest.js +572 -0
- package/dist/services/workspaceToolMutationQueue.js +43 -0
- package/dist/services/workspaceToolPanelProjection.js +460 -0
- package/dist/services/workspaceToolPromotion.js +255 -0
- package/dist/services/workspaceToolPromotionState.js +224 -0
- package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
- package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
- package/dist/services/workspaceToolReadModel.js +378 -0
- package/dist/services/workspaceToolRunLedger.js +239 -0
- package/dist/services/workspaceToolService.js +3067 -0
- package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
- package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
- package/dist/services/workspaceToolTypes.js +1 -0
- package/dist/services/workspaceToolUploadMaterializer.js +228 -0
- package/dist/web/actionCardRoutes.js +129 -0
- package/dist/web/actionCards.js +469 -0
- package/dist/web/activationContext.js +684 -0
- package/dist/web/agentChannelGuards.js +48 -0
- package/dist/web/agentMentionCooldowns.js +32 -0
- package/dist/web/agentReminders.js +1668 -0
- package/dist/web/agentRuntimePresence.js +197 -0
- package/dist/web/agentSelfState.js +494 -0
- package/dist/web/agentTaskLinks.js +26 -0
- package/dist/web/agentVisibility.js +79 -0
- package/dist/web/assets.js +95 -0
- package/dist/web/channelActivationPrompt.js +395 -0
- package/dist/web/channelMemoryNotes.js +127 -0
- package/dist/web/channelMentions.js +10 -0
- package/dist/web/channelMessageSequences.js +19 -0
- package/dist/web/channelSubscriptions.js +26 -0
- package/dist/web/clearedTaskRoots.js +10 -0
- package/dist/web/collaborationPromptGuidance.js +36 -0
- package/dist/web/collaborationSurfaceState.js +140 -0
- package/dist/web/contextBundleRanking.js +154 -0
- package/dist/web/contextBundleResolver.js +488 -0
- package/dist/web/conversationBuiltinSkillRoots.js +50 -0
- package/dist/web/conversationControls.js +232 -0
- package/dist/web/conversationHandoffs.js +612 -0
- package/dist/web/conversationManager.js +2511 -0
- package/dist/web/conversationSummaries.js +876 -0
- package/dist/web/conversationSurfaceKinds.js +17 -0
- package/dist/web/conversationTargets.js +173 -0
- package/dist/web/directActivationPrompt.js +122 -0
- package/dist/web/directReplyTargets.js +69 -0
- package/dist/web/directThreadResolver.js +129 -0
- package/dist/web/dmTaskHandoffPrompt.js +120 -0
- package/dist/web/dmTaskThreadStatusProjection.js +229 -0
- package/dist/web/ftsQuery.js +33 -0
- package/dist/web/internalAgentRouter.js +11341 -0
- package/dist/web/libraryCuratorScheduler.js +58 -0
- package/dist/web/libraryDocumentPromptGuidance.js +8 -0
- package/dist/web/messageCheckpoints.js +19 -0
- package/dist/web/nodeWsHandler.js +2495 -0
- package/dist/web/notificationRounds.js +1061 -0
- package/dist/web/panelActionMessages.js +108 -0
- package/dist/web/panelActivationPrompt.js +18 -0
- package/dist/web/panelAudit.js +273 -0
- package/dist/web/panelLifecycle.js +222 -0
- package/dist/web/panelMediaPolicy.js +43 -0
- package/dist/web/panelPathPolicy.js +63 -0
- package/dist/web/panelPreviews.js +175 -0
- package/dist/web/panelQueryHandles.js +2749 -0
- package/dist/web/panelRoutes.js +2147 -0
- package/dist/web/panels.js +904 -0
- package/dist/web/peerInboxAggregates.js +1247 -0
- package/dist/web/planApprovalState.js +92 -0
- package/dist/web/platformAgentScheduler.js +66 -0
- package/dist/web/proactiveOpportunities.js +452 -0
- package/dist/web/promptContextSections.js +242 -0
- package/dist/web/promptHistorySanitizer.js +26 -0
- package/dist/web/promptSlashCommands.js +158 -0
- package/dist/web/rollingConversationSummary.js +453 -0
- package/dist/web/routeHelpers.js +11 -0
- package/dist/web/routes/handoff.js +288 -0
- package/dist/web/routes/history.js +345 -0
- package/dist/web/routes/memory.js +258 -0
- package/dist/web/routes/selfState.js +171 -0
- package/dist/web/routes/workspace.js +154 -0
- package/dist/web/runSurfaceWatermarks.js +431 -0
- package/dist/web/runtimeCapabilities.js +48 -0
- package/dist/web/sameAgentHandoffs.js +494 -0
- package/dist/web/server.js +15567 -0
- package/dist/web/sharedCollaborationCapsules.js +163 -0
- package/dist/web/soloSessionRelay.js +42 -0
- package/dist/web/soloWsHandler.js +138 -0
- package/dist/web/suggestedPlannerScheduler.js +56 -0
- package/dist/web/surfaceActivationPolicy.js +108 -0
- package/dist/web/surfaceCollaborators.js +61 -0
- package/dist/web/surfaceSystemStatus.js +263 -0
- package/dist/web/targetParticipants.js +77 -0
- package/dist/web/taskEvents.js +49 -0
- package/dist/web/taskLifecycleMessages.js +165 -0
- package/dist/web/taskLoops.js +732 -0
- package/dist/web/taskMemoryNotes.js +224 -0
- package/dist/web/taskNumbers.js +16 -0
- package/dist/web/taskOwnerGuards.js +49 -0
- package/dist/web/taskParticipantResolver.js +42 -0
- package/dist/web/taskParticipants.js +97 -0
- package/dist/web/taskSourceDetails.js +20 -0
- package/dist/web/taskStateViews.js +210 -0
- package/dist/web/taskStatusTransitions.js +9 -0
- package/dist/web/taskThreadFollowups.js +599 -0
- package/dist/web/taskThreadRuntimeClosure.js +685 -0
- package/dist/web/taskUpdateDelivery.js +104 -0
- package/dist/web/threadReplyContentHeuristics.js +30 -0
- package/dist/web/threadRoots.js +61 -0
- package/dist/web/threadTaskBindings.js +365 -0
- package/dist/web/uiPanelPromptGuidance.js +27 -0
- package/dist/web/workspaceMemoryHints.js +143 -0
- package/dist/web/workspaceToolPromptGuidance.js +30 -0
- package/dist/web/wsHandler.js +397 -0
- package/dist/web/wsSink.js +116 -0
- package/package.json +54 -0
|
@@ -0,0 +1,2361 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { importDroidMissionArtifacts } from './droidMissionImporter.js';
|
|
6
|
+
import { generateNativeMissionReport, NATIVE_MISSION_REPORT_FILE_NAME, } from './nativeMissionReportGenerator.js';
|
|
7
|
+
import { deriveMissionTrace, filterMissionTrace } from './missionTrace.js';
|
|
8
|
+
import { deriveMissionEvidence, filterMissionEvidence } from './missionEvidence.js';
|
|
9
|
+
const DEFAULT_DROID_MODEL = 'custom:DeepSeek-V4-Pro';
|
|
10
|
+
const DEFAULT_RETRY_POLICY = { maxRetries: 0, backoff: 'fixed' };
|
|
11
|
+
export class MissionServiceError extends Error {
|
|
12
|
+
statusCode;
|
|
13
|
+
constructor(statusCode, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.statusCode = statusCode;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class MissionService {
|
|
19
|
+
db;
|
|
20
|
+
constructor(db) {
|
|
21
|
+
this.db = db;
|
|
22
|
+
}
|
|
23
|
+
listNativeMissionsForRecovery() {
|
|
24
|
+
const rows = this.db.prepare(`
|
|
25
|
+
SELECT mission_id as missionId, owner_user_id as ownerUserId, title, description, state,
|
|
26
|
+
runtime_provider as runtimeProvider, node_id as nodeId, workspace_path as workspacePath,
|
|
27
|
+
mission_dir as missionDir, initiator_agent_id as initiatorAgentId,
|
|
28
|
+
control_conversation_id as controlConversationId, model_mode as modelMode,
|
|
29
|
+
orchestrator_model as orchestratorModel, worker_model as workerModel,
|
|
30
|
+
validator_model as validatorModel, effective_model_config_json as effectiveModelConfigJson,
|
|
31
|
+
created_at as createdAt, updated_at as updatedAt, started_at as startedAt,
|
|
32
|
+
completed_at as completedAt, last_imported_at as lastImportedAt,
|
|
33
|
+
latest_factory_session_id as latestFactorySessionId,
|
|
34
|
+
latest_factory_artifact_dir as latestFactoryArtifactDir,
|
|
35
|
+
latest_factory_seen_at as latestFactorySeenAt
|
|
36
|
+
FROM missions
|
|
37
|
+
WHERE runtime_provider = 'bigbang_native'
|
|
38
|
+
AND state IN ('queued', 'running', 'paused')
|
|
39
|
+
ORDER BY updated_at DESC
|
|
40
|
+
`).all();
|
|
41
|
+
return rows.map(mapMissionRow);
|
|
42
|
+
}
|
|
43
|
+
listMissions(input) {
|
|
44
|
+
const rows = input.isAdmin
|
|
45
|
+
? this.db.prepare(`
|
|
46
|
+
SELECT mission_id as missionId, owner_user_id as ownerUserId, title, description, state,
|
|
47
|
+
runtime_provider as runtimeProvider, node_id as nodeId, workspace_path as workspacePath,
|
|
48
|
+
mission_dir as missionDir, initiator_agent_id as initiatorAgentId,
|
|
49
|
+
control_conversation_id as controlConversationId, model_mode as modelMode,
|
|
50
|
+
orchestrator_model as orchestratorModel, worker_model as workerModel,
|
|
51
|
+
validator_model as validatorModel, effective_model_config_json as effectiveModelConfigJson,
|
|
52
|
+
created_at as createdAt, updated_at as updatedAt, started_at as startedAt,
|
|
53
|
+
completed_at as completedAt, last_imported_at as lastImportedAt,
|
|
54
|
+
latest_factory_session_id as latestFactorySessionId,
|
|
55
|
+
latest_factory_artifact_dir as latestFactoryArtifactDir,
|
|
56
|
+
latest_factory_seen_at as latestFactorySeenAt
|
|
57
|
+
FROM missions
|
|
58
|
+
ORDER BY updated_at DESC
|
|
59
|
+
`).all()
|
|
60
|
+
: this.db.prepare(`
|
|
61
|
+
SELECT mission_id as missionId, owner_user_id as ownerUserId, title, description, state,
|
|
62
|
+
runtime_provider as runtimeProvider, node_id as nodeId, workspace_path as workspacePath,
|
|
63
|
+
mission_dir as missionDir, initiator_agent_id as initiatorAgentId,
|
|
64
|
+
control_conversation_id as controlConversationId, model_mode as modelMode,
|
|
65
|
+
orchestrator_model as orchestratorModel, worker_model as workerModel,
|
|
66
|
+
validator_model as validatorModel, effective_model_config_json as effectiveModelConfigJson,
|
|
67
|
+
created_at as createdAt, updated_at as updatedAt, started_at as startedAt,
|
|
68
|
+
completed_at as completedAt, last_imported_at as lastImportedAt,
|
|
69
|
+
latest_factory_session_id as latestFactorySessionId,
|
|
70
|
+
latest_factory_artifact_dir as latestFactoryArtifactDir,
|
|
71
|
+
latest_factory_seen_at as latestFactorySeenAt
|
|
72
|
+
FROM missions
|
|
73
|
+
WHERE owner_user_id = ?
|
|
74
|
+
ORDER BY updated_at DESC
|
|
75
|
+
`).all(input.userId);
|
|
76
|
+
return rows.map(mapMissionRow);
|
|
77
|
+
}
|
|
78
|
+
getMission(input) {
|
|
79
|
+
const row = this.db.prepare(`
|
|
80
|
+
SELECT mission_id as missionId, owner_user_id as ownerUserId, title, description, state,
|
|
81
|
+
runtime_provider as runtimeProvider, node_id as nodeId, workspace_path as workspacePath,
|
|
82
|
+
mission_dir as missionDir, initiator_agent_id as initiatorAgentId,
|
|
83
|
+
control_conversation_id as controlConversationId, model_mode as modelMode,
|
|
84
|
+
orchestrator_model as orchestratorModel, worker_model as workerModel,
|
|
85
|
+
validator_model as validatorModel, effective_model_config_json as effectiveModelConfigJson,
|
|
86
|
+
created_at as createdAt, updated_at as updatedAt, started_at as startedAt,
|
|
87
|
+
completed_at as completedAt, last_imported_at as lastImportedAt,
|
|
88
|
+
latest_factory_session_id as latestFactorySessionId,
|
|
89
|
+
latest_factory_artifact_dir as latestFactoryArtifactDir,
|
|
90
|
+
latest_factory_seen_at as latestFactorySeenAt
|
|
91
|
+
FROM missions
|
|
92
|
+
WHERE mission_id = ?
|
|
93
|
+
`).get(input.missionId);
|
|
94
|
+
if (!row)
|
|
95
|
+
return null;
|
|
96
|
+
if (!input.isAdmin && row.ownerUserId !== input.userId)
|
|
97
|
+
return null;
|
|
98
|
+
return mapMissionRow(row);
|
|
99
|
+
}
|
|
100
|
+
createMission(input) {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
const title = normalizeRequiredString(input.request.title, 'title');
|
|
103
|
+
const workspacePath = normalizeRequiredString(input.request.workspacePath, 'workspacePath');
|
|
104
|
+
const modelMode = normalizeModelMode(input.request.modelMode);
|
|
105
|
+
const model = modelMode === 'platform_override' ? DEFAULT_DROID_MODEL : null;
|
|
106
|
+
const runtimeProvider = hasOwn(input.request, 'runtimeProvider')
|
|
107
|
+
? normalizeMissionRuntimeProvider(input.request.runtimeProvider)
|
|
108
|
+
?? (() => {
|
|
109
|
+
throw new MissionServiceError(400, "runtimeProvider must be 'droid_cli' or 'bigbang_native'");
|
|
110
|
+
})()
|
|
111
|
+
: 'droid_cli';
|
|
112
|
+
const missionId = randomUUID();
|
|
113
|
+
const effectiveModelConfig = buildEffectiveModelConfig({
|
|
114
|
+
mode: modelMode,
|
|
115
|
+
validationPolicy: normalizeValidationPolicy(input.request.validationPolicy),
|
|
116
|
+
validationTimeoutMs: normalizeValidationTimeoutMs(input.request.validationTimeoutMs),
|
|
117
|
+
retryPolicy: normalizeRetryPolicy(input.request.retryPolicy),
|
|
118
|
+
});
|
|
119
|
+
const create = this.db.transaction(() => {
|
|
120
|
+
this.db.prepare(`
|
|
121
|
+
INSERT INTO missions (
|
|
122
|
+
mission_id, owner_user_id, title, description, state, runtime_provider, node_id,
|
|
123
|
+
workspace_path, mission_dir, initiator_agent_id, control_conversation_id, model_mode,
|
|
124
|
+
orchestrator_model, worker_model, validator_model, effective_model_config_json,
|
|
125
|
+
created_at, updated_at
|
|
126
|
+
) VALUES (?, ?, ?, ?, 'draft', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
127
|
+
`).run(missionId, input.userId, title, normalizeOptionalString(input.request.description) ?? '', runtimeProvider, normalizeOptionalString(input.request.nodeId), workspacePath, normalizeOptionalString(input.request.missionDir), normalizeOptionalString(input.request.initiatorAgentId), normalizeOptionalString(input.request.controlConversationId), modelMode, normalizeOptionalString(input.request.orchestratorModel) ?? model, normalizeOptionalString(input.request.workerModel) ?? model, normalizeOptionalString(input.request.validatorModel) ?? model, JSON.stringify(effectiveModelConfig), now, now);
|
|
128
|
+
this.appendEvent({
|
|
129
|
+
missionId,
|
|
130
|
+
eventType: 'mission_created',
|
|
131
|
+
source: 'platform',
|
|
132
|
+
eventTime: now,
|
|
133
|
+
payload: { title, workspacePath, modelMode, runtimeProvider },
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
create();
|
|
137
|
+
const mission = this.getMission({ missionId, userId: input.userId, isAdmin: true });
|
|
138
|
+
if (!mission)
|
|
139
|
+
throw new MissionServiceError(500, 'Mission was not created.');
|
|
140
|
+
return mission;
|
|
141
|
+
}
|
|
142
|
+
updateMission(input) {
|
|
143
|
+
const current = this.requireMissionAccess(input);
|
|
144
|
+
if (current.state === 'queued' || current.state === 'running') {
|
|
145
|
+
throw new MissionServiceError(409, 'Mission configuration cannot be changed while queued or running');
|
|
146
|
+
}
|
|
147
|
+
const request = input.request ?? {};
|
|
148
|
+
const modelMode = hasOwn(request, 'modelMode') ? normalizeModelMode(request.modelMode) : current.modelMode;
|
|
149
|
+
const runtimeProvider = hasOwn(request, 'runtimeProvider')
|
|
150
|
+
? normalizeMissionRuntimeProviderForUpdate(request.runtimeProvider, current)
|
|
151
|
+
: current.runtimeProvider;
|
|
152
|
+
const next = {
|
|
153
|
+
title: hasOwn(request, 'title') ? normalizeRequiredString(request.title, 'title') : current.title,
|
|
154
|
+
description: hasOwn(request, 'description') ? normalizeOptionalString(request.description) ?? '' : current.description,
|
|
155
|
+
runtimeProvider,
|
|
156
|
+
nodeId: hasOwn(request, 'nodeId') ? normalizeOptionalString(request.nodeId) : current.nodeId,
|
|
157
|
+
workspacePath: hasOwn(request, 'workspacePath')
|
|
158
|
+
? normalizeRequiredString(request.workspacePath, 'workspacePath')
|
|
159
|
+
: current.workspacePath,
|
|
160
|
+
missionDir: hasOwn(request, 'missionDir') ? normalizeOptionalString(request.missionDir) : current.missionDir,
|
|
161
|
+
initiatorAgentId: hasOwn(request, 'initiatorAgentId')
|
|
162
|
+
? normalizeOptionalString(request.initiatorAgentId)
|
|
163
|
+
: current.initiatorAgentId,
|
|
164
|
+
controlConversationId: hasOwn(request, 'controlConversationId')
|
|
165
|
+
? normalizeOptionalString(request.controlConversationId)
|
|
166
|
+
: current.controlConversationId,
|
|
167
|
+
modelMode,
|
|
168
|
+
orchestratorModel: nextModelValue(request, 'orchestratorModel', current.orchestratorModel, modelMode),
|
|
169
|
+
workerModel: nextModelValue(request, 'workerModel', current.workerModel, modelMode),
|
|
170
|
+
validatorModel: nextModelValue(request, 'validatorModel', current.validatorModel, modelMode),
|
|
171
|
+
validationPolicy: hasOwn(request, 'validationPolicy')
|
|
172
|
+
? normalizeValidationPolicy(request.validationPolicy)
|
|
173
|
+
: readValidationPolicy(current.effectiveModelConfig),
|
|
174
|
+
validationTimeoutMs: hasOwn(request, 'validationTimeoutMs')
|
|
175
|
+
? normalizeValidationTimeoutMs(request.validationTimeoutMs)
|
|
176
|
+
: readValidationTimeoutMs(current.effectiveModelConfig),
|
|
177
|
+
retryPolicy: hasOwn(request, 'retryPolicy')
|
|
178
|
+
? normalizeRetryPolicy(request.retryPolicy)
|
|
179
|
+
: readRetryPolicy(current.effectiveModelConfig),
|
|
180
|
+
};
|
|
181
|
+
const changedFields = collectChangedMissionFields(current, next);
|
|
182
|
+
const effectiveModelConfig = buildEffectiveModelConfig({
|
|
183
|
+
mode: next.modelMode,
|
|
184
|
+
validationPolicy: next.validationPolicy,
|
|
185
|
+
validationTimeoutMs: next.validationTimeoutMs,
|
|
186
|
+
retryPolicy: next.retryPolicy,
|
|
187
|
+
});
|
|
188
|
+
const effectiveConfigChanged = JSON.stringify(current.effectiveModelConfig) !== JSON.stringify(effectiveModelConfig);
|
|
189
|
+
if (effectiveConfigChanged) {
|
|
190
|
+
if (next.validationPolicy !== readValidationPolicy(current.effectiveModelConfig)) {
|
|
191
|
+
changedFields.push('validationPolicy');
|
|
192
|
+
}
|
|
193
|
+
if (next.validationTimeoutMs !== readValidationTimeoutMs(current.effectiveModelConfig)) {
|
|
194
|
+
changedFields.push('validationTimeoutMs');
|
|
195
|
+
}
|
|
196
|
+
if (JSON.stringify(next.retryPolicy) !== JSON.stringify(readRetryPolicy(current.effectiveModelConfig))) {
|
|
197
|
+
changedFields.push('retryPolicy');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (changedFields.length === 0)
|
|
201
|
+
return current;
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
this.db.prepare(`
|
|
204
|
+
UPDATE missions
|
|
205
|
+
SET title = ?, description = ?, runtime_provider = ?, node_id = ?, workspace_path = ?, mission_dir = ?,
|
|
206
|
+
initiator_agent_id = ?, control_conversation_id = ?, model_mode = ?,
|
|
207
|
+
orchestrator_model = ?, worker_model = ?, validator_model = ?,
|
|
208
|
+
effective_model_config_json = ?, updated_at = ?
|
|
209
|
+
WHERE mission_id = ?
|
|
210
|
+
`).run(next.title, next.description, next.runtimeProvider, next.nodeId, next.workspacePath, next.missionDir, next.initiatorAgentId, next.controlConversationId, next.modelMode, next.orchestratorModel, next.workerModel, next.validatorModel, JSON.stringify(effectiveModelConfig), now, input.missionId);
|
|
211
|
+
this.appendEvent({
|
|
212
|
+
missionId: input.missionId,
|
|
213
|
+
eventType: 'mission_updated',
|
|
214
|
+
source: 'platform',
|
|
215
|
+
eventTime: now,
|
|
216
|
+
payload: { fields: changedFields },
|
|
217
|
+
});
|
|
218
|
+
const updated = this.getMission({ missionId: input.missionId, userId: input.userId, isAdmin: true });
|
|
219
|
+
if (!updated)
|
|
220
|
+
throw new MissionServiceError(404, 'Mission not found');
|
|
221
|
+
return updated;
|
|
222
|
+
}
|
|
223
|
+
startMission(input) {
|
|
224
|
+
const mission = this.requireMissionAccess(input);
|
|
225
|
+
if (mission.state === 'queued' || mission.state === 'running') {
|
|
226
|
+
throw new MissionServiceError(409, 'Mission is already queued or running');
|
|
227
|
+
}
|
|
228
|
+
const nodeId = normalizeOptionalString(input.nodeId) ?? mission.nodeId;
|
|
229
|
+
if (!nodeId)
|
|
230
|
+
throw new MissionServiceError(400, 'nodeId is required');
|
|
231
|
+
if (!mission.missionDir)
|
|
232
|
+
throw new MissionServiceError(400, 'missionDir is required');
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
this.db.prepare(`
|
|
235
|
+
UPDATE missions
|
|
236
|
+
SET state = 'queued', node_id = ?, updated_at = ?
|
|
237
|
+
WHERE mission_id = ?
|
|
238
|
+
`).run(nodeId, now, input.missionId);
|
|
239
|
+
this.appendEvent({
|
|
240
|
+
missionId: input.missionId,
|
|
241
|
+
eventType: input.eventType ?? 'mission_queued',
|
|
242
|
+
source: 'platform',
|
|
243
|
+
eventTime: now,
|
|
244
|
+
payload: { nodeId, ...(input.eventPayload ?? {}) },
|
|
245
|
+
});
|
|
246
|
+
const updated = this.getMission({ missionId: input.missionId, userId: input.userId, isAdmin: true });
|
|
247
|
+
if (!updated)
|
|
248
|
+
throw new MissionServiceError(404, 'Mission not found');
|
|
249
|
+
return updated;
|
|
250
|
+
}
|
|
251
|
+
pauseMission(input) {
|
|
252
|
+
const mission = this.requireMissionAccess(input);
|
|
253
|
+
if (mission.state !== 'running') {
|
|
254
|
+
throw new MissionServiceError(409, 'Mission can only be paused while running');
|
|
255
|
+
}
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
this.db.prepare(`
|
|
258
|
+
UPDATE missions
|
|
259
|
+
SET state = 'paused', updated_at = ?
|
|
260
|
+
WHERE mission_id = ?
|
|
261
|
+
`).run(now, input.missionId);
|
|
262
|
+
this.appendEvent({
|
|
263
|
+
missionId: input.missionId,
|
|
264
|
+
eventType: 'mission_paused',
|
|
265
|
+
source: 'platform',
|
|
266
|
+
eventTime: now,
|
|
267
|
+
payload: {
|
|
268
|
+
reason: input.reason ?? null,
|
|
269
|
+
effectivePausePoint: input.effectivePausePoint ?? null,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
const updated = this.getMission({ missionId: input.missionId, userId: input.userId, isAdmin: true });
|
|
273
|
+
if (!updated)
|
|
274
|
+
throw new MissionServiceError(404, 'Mission not found');
|
|
275
|
+
return updated;
|
|
276
|
+
}
|
|
277
|
+
resumeMission(input) {
|
|
278
|
+
const mission = this.requireMissionAccess(input);
|
|
279
|
+
if (mission.state !== 'paused') {
|
|
280
|
+
throw new MissionServiceError(409, 'Mission can only be resumed from paused');
|
|
281
|
+
}
|
|
282
|
+
const now = Date.now();
|
|
283
|
+
this.db.prepare(`
|
|
284
|
+
UPDATE missions
|
|
285
|
+
SET state = 'running', updated_at = ?
|
|
286
|
+
WHERE mission_id = ?
|
|
287
|
+
`).run(now, input.missionId);
|
|
288
|
+
this.appendEvent({
|
|
289
|
+
missionId: input.missionId,
|
|
290
|
+
eventType: 'mission_resumed',
|
|
291
|
+
source: 'platform',
|
|
292
|
+
eventTime: now,
|
|
293
|
+
payload: {},
|
|
294
|
+
});
|
|
295
|
+
const updated = this.getMission({ missionId: input.missionId, userId: input.userId, isAdmin: true });
|
|
296
|
+
if (!updated)
|
|
297
|
+
throw new MissionServiceError(404, 'Mission not found');
|
|
298
|
+
return updated;
|
|
299
|
+
}
|
|
300
|
+
cancelMission(input) {
|
|
301
|
+
const mission = this.requireMissionAccess(input);
|
|
302
|
+
if (!['queued', 'running', 'paused'].includes(mission.state)) {
|
|
303
|
+
throw new MissionServiceError(409, 'Mission can only be cancelled while queued, running, or paused');
|
|
304
|
+
}
|
|
305
|
+
const now = Date.now();
|
|
306
|
+
this.db.prepare(`
|
|
307
|
+
UPDATE missions
|
|
308
|
+
SET state = 'cancelled', completed_at = ?, updated_at = ?
|
|
309
|
+
WHERE mission_id = ?
|
|
310
|
+
`).run(now, now, input.missionId);
|
|
311
|
+
this.appendEvent({
|
|
312
|
+
missionId: input.missionId,
|
|
313
|
+
eventType: 'mission_cancelled',
|
|
314
|
+
source: 'platform',
|
|
315
|
+
eventTime: now,
|
|
316
|
+
payload: {},
|
|
317
|
+
});
|
|
318
|
+
const updated = this.getMission({ missionId: input.missionId, userId: input.userId, isAdmin: true });
|
|
319
|
+
if (!updated)
|
|
320
|
+
throw new MissionServiceError(404, 'Mission not found');
|
|
321
|
+
return updated;
|
|
322
|
+
}
|
|
323
|
+
deleteMission(input) {
|
|
324
|
+
this.requireMissionAccess(input);
|
|
325
|
+
this.db.prepare(`DELETE FROM missions WHERE mission_id = ?`).run(input.missionId);
|
|
326
|
+
}
|
|
327
|
+
completeMission(input) {
|
|
328
|
+
const mission = this.requireMissionAccess(input);
|
|
329
|
+
if (mission.state !== 'running') {
|
|
330
|
+
throw new MissionServiceError(409, 'Mission can only be completed while running');
|
|
331
|
+
}
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
this.db.prepare(`
|
|
334
|
+
UPDATE missions
|
|
335
|
+
SET state = 'completed', completed_at = ?, updated_at = ?
|
|
336
|
+
WHERE mission_id = ?
|
|
337
|
+
`).run(now, now, input.missionId);
|
|
338
|
+
this.appendEvent({
|
|
339
|
+
missionId: input.missionId,
|
|
340
|
+
eventType: 'mission_completed',
|
|
341
|
+
source: 'agent_node',
|
|
342
|
+
eventTime: now,
|
|
343
|
+
payload: input.payload ?? {},
|
|
344
|
+
});
|
|
345
|
+
const updated = this.getMission({ missionId: input.missionId, userId: input.userId, isAdmin: true });
|
|
346
|
+
if (!updated)
|
|
347
|
+
throw new MissionServiceError(404, 'Mission not found');
|
|
348
|
+
return updated;
|
|
349
|
+
}
|
|
350
|
+
failMission(input) {
|
|
351
|
+
const mission = this.requireMissionAccess(input);
|
|
352
|
+
if (!['queued', 'running', 'paused'].includes(mission.state)) {
|
|
353
|
+
throw new MissionServiceError(409, 'Mission cannot be failed from its current state');
|
|
354
|
+
}
|
|
355
|
+
const now = Date.now();
|
|
356
|
+
this.db.prepare(`
|
|
357
|
+
UPDATE missions
|
|
358
|
+
SET state = 'failed', completed_at = ?, updated_at = ?
|
|
359
|
+
WHERE mission_id = ?
|
|
360
|
+
`).run(now, now, input.missionId);
|
|
361
|
+
this.appendEvent({
|
|
362
|
+
missionId: input.missionId,
|
|
363
|
+
eventType: 'mission_failed',
|
|
364
|
+
source: 'agent_node',
|
|
365
|
+
eventTime: now,
|
|
366
|
+
payload: { error: input.error ?? null, ...(input.payload ?? {}) },
|
|
367
|
+
});
|
|
368
|
+
const updated = this.getMission({ missionId: input.missionId, userId: input.userId, isAdmin: true });
|
|
369
|
+
if (!updated)
|
|
370
|
+
throw new MissionServiceError(404, 'Mission not found');
|
|
371
|
+
return updated;
|
|
372
|
+
}
|
|
373
|
+
markMissionStartFailed(input) {
|
|
374
|
+
const now = Date.now();
|
|
375
|
+
this.db.prepare(`
|
|
376
|
+
UPDATE missions
|
|
377
|
+
SET state = 'failed', completed_at = ?, updated_at = ?
|
|
378
|
+
WHERE mission_id = ?
|
|
379
|
+
`).run(now, now, input.missionId);
|
|
380
|
+
this.appendEvent({
|
|
381
|
+
missionId: input.missionId,
|
|
382
|
+
eventType: 'mission_start_failed',
|
|
383
|
+
source: 'platform',
|
|
384
|
+
eventTime: now,
|
|
385
|
+
payload: { error: input.error, requestId: input.requestId ?? null },
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
markMissionContinueStartFailed(input) {
|
|
389
|
+
const now = Date.now();
|
|
390
|
+
this.db.prepare(`
|
|
391
|
+
UPDATE missions
|
|
392
|
+
SET state = ?, completed_at = ?, updated_at = ?
|
|
393
|
+
WHERE mission_id = ?
|
|
394
|
+
`).run(input.previousState, input.previousCompletedAt, now, input.missionId);
|
|
395
|
+
this.markAttemptFailedByRequestId({
|
|
396
|
+
missionId: input.missionId,
|
|
397
|
+
requestId: input.requestId,
|
|
398
|
+
error: input.error,
|
|
399
|
+
completedAt: now,
|
|
400
|
+
});
|
|
401
|
+
this.appendEvent({
|
|
402
|
+
missionId: input.missionId,
|
|
403
|
+
eventType: 'mission_continue_start_failed',
|
|
404
|
+
source: 'platform',
|
|
405
|
+
eventTime: now,
|
|
406
|
+
payload: { error: input.error, requestId: input.requestId ?? null },
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
markMissionAccepted(input) {
|
|
410
|
+
const now = Date.now();
|
|
411
|
+
this.db.prepare(`
|
|
412
|
+
UPDATE missions
|
|
413
|
+
SET state = 'running', mission_dir = ?, started_at = COALESCE(started_at, ?), updated_at = ?
|
|
414
|
+
WHERE mission_id = ?
|
|
415
|
+
`).run(input.missionDir, now, now, input.missionId);
|
|
416
|
+
this.markAttemptAccepted({
|
|
417
|
+
missionId: input.missionId,
|
|
418
|
+
requestId: input.requestId,
|
|
419
|
+
missionDir: input.missionDir,
|
|
420
|
+
pid: input.pid,
|
|
421
|
+
acceptedAt: now,
|
|
422
|
+
});
|
|
423
|
+
this.appendEvent({
|
|
424
|
+
missionId: input.missionId,
|
|
425
|
+
eventType: 'mission_run_accepted',
|
|
426
|
+
source: 'agent_node',
|
|
427
|
+
eventTime: now,
|
|
428
|
+
payload: {
|
|
429
|
+
missionDir: input.missionDir,
|
|
430
|
+
pid: input.pid ?? null,
|
|
431
|
+
requestId: input.requestId ?? null,
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
recordMissionRuntimeEvent(input) {
|
|
436
|
+
if (input.eventType === 'mission_interaction_accepted') {
|
|
437
|
+
this.markAttemptAccepted({
|
|
438
|
+
missionId: input.missionId,
|
|
439
|
+
requestId: asPayloadString(input.payload, 'requestId'),
|
|
440
|
+
missionDir: asPayloadString(input.payload, 'missionDir'),
|
|
441
|
+
pid: asPayloadNumber(input.payload, 'pid'),
|
|
442
|
+
acceptedAt: input.eventTime,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
else if (input.eventType === 'mission_interaction_completed' || input.eventType === 'mission_interaction_failed') {
|
|
446
|
+
this.markAttemptEnded({
|
|
447
|
+
missionId: input.missionId,
|
|
448
|
+
requestId: asPayloadString(input.payload, 'requestId'),
|
|
449
|
+
missionDir: asPayloadString(input.payload, 'missionDir'),
|
|
450
|
+
factorySessionId: asPayloadString(input.payload, 'factorySessionId'),
|
|
451
|
+
factoryArtifactDir: asPayloadString(input.payload, 'factoryArtifactDir'),
|
|
452
|
+
exitCode: asPayloadNumber(input.payload, 'exitCode'),
|
|
453
|
+
signal: asPayloadString(input.payload, 'signal'),
|
|
454
|
+
error: asPayloadString(input.payload, 'error'),
|
|
455
|
+
completedAt: input.eventTime,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
else if (input.eventType === 'mission_interaction_start_failed') {
|
|
459
|
+
this.markAttemptFailedByRequestId({
|
|
460
|
+
missionId: input.missionId,
|
|
461
|
+
requestId: asPayloadString(input.payload, 'requestId'),
|
|
462
|
+
error: asPayloadString(input.payload, 'error') ?? 'Droid mission interaction did not start.',
|
|
463
|
+
completedAt: input.eventTime,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
else if (input.eventType === 'mission_attempt_start_failed') {
|
|
467
|
+
this.markAttemptFailedByRequestId({
|
|
468
|
+
missionId: input.missionId,
|
|
469
|
+
requestId: asPayloadString(input.payload, 'requestId'),
|
|
470
|
+
error: asPayloadString(input.payload, 'error') ?? 'Droid mission did not start.',
|
|
471
|
+
completedAt: input.eventTime,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
else if (input.eventType === 'mission_stdout' || input.eventType === 'mission_stderr') {
|
|
475
|
+
this.recordAttemptOutput({
|
|
476
|
+
missionId: input.missionId,
|
|
477
|
+
requestId: asPayloadString(input.payload, 'requestId'),
|
|
478
|
+
stream: input.eventType === 'mission_stdout' ? 'stdout' : 'stderr',
|
|
479
|
+
text: asPayloadString(input.payload, 'text') ?? '',
|
|
480
|
+
eventTime: input.eventTime,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
this.persistFactoryEvidenceFromPayload(input.missionId, input.payload, input.eventTime);
|
|
484
|
+
this.appendEvent(input);
|
|
485
|
+
}
|
|
486
|
+
recordPhase1ClosureDecision(input) {
|
|
487
|
+
const mission = this.requireMissionAccess(input);
|
|
488
|
+
if (input.request?.decision !== 'accepted_paused_partial_boundary') {
|
|
489
|
+
throw new MissionServiceError(400, 'Unsupported Phase 1 closure decision.');
|
|
490
|
+
}
|
|
491
|
+
if (!this.isPhase1PausedPartialBoundaryEligible(mission)) {
|
|
492
|
+
throw new MissionServiceError(409, 'Phase 1 paused partial boundary is not eligible for acceptance.');
|
|
493
|
+
}
|
|
494
|
+
const now = Date.now();
|
|
495
|
+
const note = normalizeOptionalString(input.request.note);
|
|
496
|
+
const eventId = this.appendEvent({
|
|
497
|
+
missionId: input.missionId,
|
|
498
|
+
eventType: 'mission_phase1_closure_decision',
|
|
499
|
+
source: 'platform',
|
|
500
|
+
eventTime: now,
|
|
501
|
+
payload: {
|
|
502
|
+
phase: 'phase1',
|
|
503
|
+
decision: 'accepted_paused_partial_boundary',
|
|
504
|
+
closureStatus: 'partial_boundary',
|
|
505
|
+
reason: 'Operator accepted the documented paused partial boundary; this does not mark the Droid run completed.',
|
|
506
|
+
note: note ?? null,
|
|
507
|
+
acceptedByUserId: input.userId,
|
|
508
|
+
acceptedAt: now,
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
const event = this.getEventById(eventId);
|
|
512
|
+
if (!event)
|
|
513
|
+
throw new MissionServiceError(500, 'Phase 1 closure decision was not recorded.');
|
|
514
|
+
return event;
|
|
515
|
+
}
|
|
516
|
+
finishDroidMission(input) {
|
|
517
|
+
const now = Date.now();
|
|
518
|
+
const failed = Boolean(input.error) || (input.exitCode != null && input.exitCode !== 0) || Boolean(input.signal);
|
|
519
|
+
const factorySessionId = normalizeOptionalString(input.factorySessionId);
|
|
520
|
+
const factoryArtifactDir = normalizeOptionalString(input.factoryArtifactDir);
|
|
521
|
+
const trustedCompletionFactoryDir = resolveTrustedFactoryArtifactDir(factorySessionId, factoryArtifactDir);
|
|
522
|
+
const trustedCompletionFactorySessionId = trustedCompletionFactoryDir ? path.basename(trustedCompletionFactoryDir) : null;
|
|
523
|
+
this.db.prepare(`
|
|
524
|
+
UPDATE missions
|
|
525
|
+
SET state = ?, completed_at = ?, updated_at = ?, mission_dir = ?,
|
|
526
|
+
latest_factory_session_id = COALESCE(?, latest_factory_session_id),
|
|
527
|
+
latest_factory_artifact_dir = COALESCE(?, latest_factory_artifact_dir),
|
|
528
|
+
latest_factory_seen_at = CASE WHEN ? IS NOT NULL OR ? IS NOT NULL THEN ? ELSE latest_factory_seen_at END
|
|
529
|
+
WHERE mission_id = ?
|
|
530
|
+
`).run(failed ? 'failed' : 'completed', now, now, input.missionDir, trustedCompletionFactorySessionId, trustedCompletionFactoryDir, trustedCompletionFactorySessionId, trustedCompletionFactoryDir, now, input.missionId);
|
|
531
|
+
this.markAttemptEnded({
|
|
532
|
+
missionId: input.missionId,
|
|
533
|
+
requestId: input.requestId,
|
|
534
|
+
missionDir: input.missionDir,
|
|
535
|
+
factorySessionId,
|
|
536
|
+
factoryArtifactDir,
|
|
537
|
+
exitCode: input.exitCode,
|
|
538
|
+
signal: input.signal,
|
|
539
|
+
error: input.error,
|
|
540
|
+
completedAt: now,
|
|
541
|
+
});
|
|
542
|
+
this.appendEvent({
|
|
543
|
+
missionId: input.missionId,
|
|
544
|
+
eventType: failed ? 'mission_failed' : 'mission_completed',
|
|
545
|
+
source: 'agent_node',
|
|
546
|
+
eventTime: now,
|
|
547
|
+
payload: {
|
|
548
|
+
missionDir: input.missionDir,
|
|
549
|
+
factorySessionId: factorySessionId ?? null,
|
|
550
|
+
factoryArtifactDir: factoryArtifactDir ?? null,
|
|
551
|
+
exitCode: input.exitCode ?? null,
|
|
552
|
+
signal: input.signal ?? null,
|
|
553
|
+
error: input.error ?? null,
|
|
554
|
+
requestId: input.requestId ?? null,
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
if (!failed) {
|
|
558
|
+
try {
|
|
559
|
+
const importDir = trustedCompletionFactoryDir
|
|
560
|
+
?? input.missionDir;
|
|
561
|
+
const snapshot = importDroidMissionArtifacts(importDir, Date.now(), {
|
|
562
|
+
platformMissionDir: input.missionDir,
|
|
563
|
+
factorySessionId: trustedCompletionFactorySessionId,
|
|
564
|
+
fallbackMissionId: input.missionId,
|
|
565
|
+
fallbackState: 'completed',
|
|
566
|
+
});
|
|
567
|
+
assertDroidSnapshotImportable(snapshot, importDir);
|
|
568
|
+
this.importSnapshot({
|
|
569
|
+
missionId: input.missionId,
|
|
570
|
+
userId: '',
|
|
571
|
+
isAdmin: true,
|
|
572
|
+
request: { source: 'droid_cli', snapshot },
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
const message = String(error.message ?? error);
|
|
577
|
+
this.appendEvent({
|
|
578
|
+
missionId: input.missionId,
|
|
579
|
+
eventType: 'mission_import_failed',
|
|
580
|
+
source: 'platform',
|
|
581
|
+
eventTime: Date.now(),
|
|
582
|
+
payload: { error: message },
|
|
583
|
+
});
|
|
584
|
+
this.importSnapshot({
|
|
585
|
+
missionId: input.missionId,
|
|
586
|
+
userId: '',
|
|
587
|
+
isAdmin: true,
|
|
588
|
+
request: {
|
|
589
|
+
source: 'droid_cli_runtime',
|
|
590
|
+
snapshot: this.buildRuntimeFallbackSnapshot(input.missionId, input.missionDir, message),
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
listEvents(input) {
|
|
597
|
+
this.requireMissionAccess(input);
|
|
598
|
+
const afterCreatedAt = typeof input.afterCreatedAt === 'number' && Number.isFinite(input.afterCreatedAt)
|
|
599
|
+
? Math.max(0, Math.floor(input.afterCreatedAt))
|
|
600
|
+
: null;
|
|
601
|
+
const limit = typeof input.limit === 'number' && Number.isFinite(input.limit)
|
|
602
|
+
? Math.min(1_000, Math.max(1, Math.floor(input.limit)))
|
|
603
|
+
: null;
|
|
604
|
+
const rows = this.db.prepare(`
|
|
605
|
+
SELECT event_id as eventId, mission_id as missionId, event_type as eventType,
|
|
606
|
+
source, event_time as eventTime, payload_json as payloadJson, created_at as createdAt
|
|
607
|
+
FROM mission_events
|
|
608
|
+
WHERE mission_id = ?
|
|
609
|
+
AND (? IS NULL OR created_at > ?)
|
|
610
|
+
ORDER BY event_time ASC, created_at ASC, rowid ASC
|
|
611
|
+
LIMIT ?
|
|
612
|
+
`).all(input.missionId, afterCreatedAt, afterCreatedAt, limit ?? -1);
|
|
613
|
+
return rows.map(mapEventRow);
|
|
614
|
+
}
|
|
615
|
+
listAttempts(input) {
|
|
616
|
+
this.requireMissionAccess(input);
|
|
617
|
+
const rows = this.db.prepare(`
|
|
618
|
+
SELECT attempt_id as attemptId, mission_id as missionId, request_id as requestId,
|
|
619
|
+
attempt_type as attemptType, state, node_id as nodeId, mission_dir as missionDir,
|
|
620
|
+
prompt_preview as promptPreview, pid, exit_code as exitCode, signal, error,
|
|
621
|
+
factory_session_id as factorySessionId, factory_artifact_dir as factoryArtifactDir,
|
|
622
|
+
stdout_event_count as stdoutEventCount, stderr_event_count as stderrEventCount,
|
|
623
|
+
output_preview as outputPreview, last_output_at as lastOutputAt,
|
|
624
|
+
superseded_by as supersededBy, supersession_reason as supersessionReason,
|
|
625
|
+
superseded_at as supersededAt,
|
|
626
|
+
created_at as createdAt, accepted_at as acceptedAt, completed_at as completedAt,
|
|
627
|
+
updated_at as updatedAt
|
|
628
|
+
FROM mission_attempts
|
|
629
|
+
WHERE mission_id = ?
|
|
630
|
+
ORDER BY created_at ASC, rowid ASC
|
|
631
|
+
`).all(input.missionId);
|
|
632
|
+
return rows.map(mapAttemptRow);
|
|
633
|
+
}
|
|
634
|
+
listMissionFeatures(input) {
|
|
635
|
+
this.requireMissionAccess(input);
|
|
636
|
+
const rows = this.db.prepare(`
|
|
637
|
+
SELECT feature_id as featureId, mission_id as missionId, milestone, description,
|
|
638
|
+
skill_name as skillName, status, ordering,
|
|
639
|
+
preconditions as preconditionsJson, expected_behavior as expectedBehaviorJson,
|
|
640
|
+
handoff as handoffJson, worker_output as workerOutputJson, retry_count as retryCount,
|
|
641
|
+
is_validator as isValidator,
|
|
642
|
+
created_at as createdAt, updated_at as updatedAt
|
|
643
|
+
FROM mission_features
|
|
644
|
+
WHERE mission_id = ?
|
|
645
|
+
ORDER BY ordering ASC, created_at ASC, rowid ASC
|
|
646
|
+
`).all(input.missionId);
|
|
647
|
+
return rows.map(mapFeatureRow);
|
|
648
|
+
}
|
|
649
|
+
replaceMissionFeatures(input) {
|
|
650
|
+
this.requireMissionAccess(input);
|
|
651
|
+
const now = Date.now();
|
|
652
|
+
const insert = this.db.transaction(() => {
|
|
653
|
+
this.db.prepare(`DELETE FROM mission_features WHERE mission_id = ?`).run(input.missionId);
|
|
654
|
+
for (const feature of input.features) {
|
|
655
|
+
this.db.prepare(`
|
|
656
|
+
INSERT INTO mission_features (
|
|
657
|
+
feature_id, mission_id, milestone, description, skill_name, status, ordering,
|
|
658
|
+
preconditions, expected_behavior, handoff, worker_output, is_validator, created_at, updated_at
|
|
659
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
660
|
+
`).run(normalizeOptionalString(feature.featureId) ?? randomUUID(), input.missionId, feature.milestone, feature.description, normalizeOptionalString(feature.skillName), normalizeNativeFeatureStatus(feature.status), feature.ordering, JSON.stringify(feature.preconditions ?? []), JSON.stringify(feature.expectedBehavior ?? []), JSON.stringify(feature.handoff ?? null), JSON.stringify(feature.workerOutput ?? null), feature.isValidator ? 1 : 0, now, now);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
insert();
|
|
664
|
+
return this.listMissionFeatures({ missionId: input.missionId, userId: input.userId, isAdmin: input.isAdmin });
|
|
665
|
+
}
|
|
666
|
+
getNextMissionFeature(input) {
|
|
667
|
+
const row = this.db.prepare(`
|
|
668
|
+
SELECT feature_id as featureId, mission_id as missionId, milestone, description,
|
|
669
|
+
skill_name as skillName, status, ordering,
|
|
670
|
+
preconditions as preconditionsJson, expected_behavior as expectedBehaviorJson,
|
|
671
|
+
handoff as handoffJson, worker_output as workerOutputJson, retry_count as retryCount,
|
|
672
|
+
is_validator as isValidator,
|
|
673
|
+
created_at as createdAt, updated_at as updatedAt
|
|
674
|
+
FROM mission_features
|
|
675
|
+
WHERE mission_id = ?
|
|
676
|
+
AND status = 'pending'
|
|
677
|
+
ORDER BY ordering ASC, created_at ASC, rowid ASC
|
|
678
|
+
LIMIT 1
|
|
679
|
+
`).get(input.missionId);
|
|
680
|
+
return row ? mapFeatureRow(row) : null;
|
|
681
|
+
}
|
|
682
|
+
listMissionValidationAssertions(input) {
|
|
683
|
+
this.requireMissionAccess(input);
|
|
684
|
+
const rows = this.db.prepare(`
|
|
685
|
+
SELECT assertion_id as assertionId, mission_id as missionId, feature_id as featureId,
|
|
686
|
+
assertion_text as assertionText, status, note, evidence, confidence,
|
|
687
|
+
created_at as createdAt, updated_at as updatedAt
|
|
688
|
+
FROM mission_validation_assertions
|
|
689
|
+
WHERE mission_id = ?
|
|
690
|
+
ORDER BY created_at ASC, rowid ASC
|
|
691
|
+
`).all(input.missionId);
|
|
692
|
+
return rows.map(mapAssertionRow);
|
|
693
|
+
}
|
|
694
|
+
replaceMissionValidationAssertions(input) {
|
|
695
|
+
this.requireMissionAccess(input);
|
|
696
|
+
const now = Date.now();
|
|
697
|
+
const insert = this.db.transaction(() => {
|
|
698
|
+
this.db.prepare(`DELETE FROM mission_validation_assertions WHERE mission_id = ?`).run(input.missionId);
|
|
699
|
+
for (const assertion of input.assertions) {
|
|
700
|
+
this.db.prepare(`
|
|
701
|
+
INSERT INTO mission_validation_assertions (
|
|
702
|
+
assertion_id, mission_id, feature_id, assertion_text, status, note, evidence, confidence,
|
|
703
|
+
created_at, updated_at
|
|
704
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
705
|
+
`).run(randomUUID(), input.missionId, normalizeOptionalString(assertion.featureId), assertion.assertionText, normalizeNativeAssertionStatus(assertion.status), normalizeOptionalString(assertion.note), normalizeOptionalString(assertion.evidence), normalizeMissionTraceConfidence(assertion.confidence), now, now);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
insert();
|
|
709
|
+
return this.listMissionValidationAssertions({ missionId: input.missionId, userId: input.userId, isAdmin: input.isAdmin });
|
|
710
|
+
}
|
|
711
|
+
generateValidationAssertionsFromFeatures(input) {
|
|
712
|
+
this.requireMissionAccess(input);
|
|
713
|
+
const features = this.listMissionFeatures({
|
|
714
|
+
missionId: input.missionId,
|
|
715
|
+
userId: input.userId,
|
|
716
|
+
isAdmin: input.isAdmin,
|
|
717
|
+
});
|
|
718
|
+
const assertions = [];
|
|
719
|
+
for (const feature of features) {
|
|
720
|
+
for (const expectedBehavior of feature.expectedBehavior) {
|
|
721
|
+
assertions.push({
|
|
722
|
+
featureId: feature.featureId,
|
|
723
|
+
assertionText: expectedBehavior,
|
|
724
|
+
status: 'pending',
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return this.replaceMissionValidationAssertions({
|
|
729
|
+
missionId: input.missionId,
|
|
730
|
+
userId: input.userId,
|
|
731
|
+
isAdmin: input.isAdmin,
|
|
732
|
+
assertions,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
queueMissionInteraction(input) {
|
|
736
|
+
this.requireMissionAccess(input);
|
|
737
|
+
const now = Date.now();
|
|
738
|
+
const interactionId = randomUUID();
|
|
739
|
+
const positionRow = this.db.prepare(`
|
|
740
|
+
SELECT COALESCE(MAX(position), 0) as maxPosition
|
|
741
|
+
FROM mission_interactions
|
|
742
|
+
WHERE mission_id = ?
|
|
743
|
+
`).get(input.missionId);
|
|
744
|
+
const position = (positionRow?.maxPosition ?? 0) + 1;
|
|
745
|
+
this.db.prepare(`
|
|
746
|
+
INSERT INTO mission_interactions (
|
|
747
|
+
interaction_id, mission_id, prompt, status, position, created_at, updated_at
|
|
748
|
+
) VALUES (?, ?, ?, 'queued', ?, ?, ?)
|
|
749
|
+
`).run(interactionId, input.missionId, input.prompt, position, now, now);
|
|
750
|
+
this.appendEvent({
|
|
751
|
+
missionId: input.missionId,
|
|
752
|
+
eventType: 'mission_interaction_queued',
|
|
753
|
+
source: 'platform',
|
|
754
|
+
eventTime: now,
|
|
755
|
+
payload: { interactionId, prompt: input.prompt, position },
|
|
756
|
+
});
|
|
757
|
+
this.appendEvent({
|
|
758
|
+
missionId: input.missionId,
|
|
759
|
+
eventType: 'mission_interaction_accepted',
|
|
760
|
+
source: 'platform',
|
|
761
|
+
eventTime: now,
|
|
762
|
+
payload: { interactionId, prompt: input.prompt, position },
|
|
763
|
+
});
|
|
764
|
+
const interaction = this.getMissionInteractionById(interactionId);
|
|
765
|
+
if (!interaction)
|
|
766
|
+
throw new MissionServiceError(500, 'Interaction was not created.');
|
|
767
|
+
return interaction;
|
|
768
|
+
}
|
|
769
|
+
listMissionInteractions(input) {
|
|
770
|
+
this.requireMissionAccess(input);
|
|
771
|
+
const rows = this.db.prepare(`
|
|
772
|
+
SELECT interaction_id as interactionId, mission_id as missionId, prompt, status,
|
|
773
|
+
position, created_at as createdAt, updated_at as updatedAt
|
|
774
|
+
FROM mission_interactions
|
|
775
|
+
WHERE mission_id = ?
|
|
776
|
+
ORDER BY position ASC, created_at ASC, rowid ASC
|
|
777
|
+
`).all(input.missionId);
|
|
778
|
+
return rows.map(mapInteractionRow);
|
|
779
|
+
}
|
|
780
|
+
getMissionPlan(input) {
|
|
781
|
+
this.requireMissionAccess(input);
|
|
782
|
+
const row = this.db.prepare(`
|
|
783
|
+
SELECT mission_id as missionId, plan_json as planJson, version,
|
|
784
|
+
created_at as createdAt, updated_at as updatedAt
|
|
785
|
+
FROM mission_plans
|
|
786
|
+
WHERE mission_id = ?
|
|
787
|
+
`).get(input.missionId);
|
|
788
|
+
return row ? mapPlanRow(row) : null;
|
|
789
|
+
}
|
|
790
|
+
replaceMissionPlan(input) {
|
|
791
|
+
this.requireMissionAccess(input);
|
|
792
|
+
const existing = this.getMissionPlan({ missionId: input.missionId, userId: input.userId, isAdmin: input.isAdmin });
|
|
793
|
+
const now = Date.now();
|
|
794
|
+
const version = (existing?.version ?? 0) + 1;
|
|
795
|
+
this.db.prepare(`
|
|
796
|
+
INSERT INTO mission_plans (mission_id, plan_json, version, created_at, updated_at)
|
|
797
|
+
VALUES (?, ?, ?, ?, ?)
|
|
798
|
+
ON CONFLICT(mission_id) DO UPDATE SET
|
|
799
|
+
plan_json = excluded.plan_json,
|
|
800
|
+
version = excluded.version,
|
|
801
|
+
updated_at = excluded.updated_at
|
|
802
|
+
`).run(input.missionId, JSON.stringify(input.plan), version, now, now);
|
|
803
|
+
const updated = this.getMissionPlan({ missionId: input.missionId, userId: input.userId, isAdmin: input.isAdmin });
|
|
804
|
+
if (!updated)
|
|
805
|
+
throw new MissionServiceError(500, 'Mission plan was not persisted.');
|
|
806
|
+
return updated;
|
|
807
|
+
}
|
|
808
|
+
listMissionMilestones(input) {
|
|
809
|
+
this.requireMissionAccess(input);
|
|
810
|
+
const rows = this.db.prepare(`
|
|
811
|
+
SELECT milestone_id as milestoneId, mission_id as missionId, name, status, ordering,
|
|
812
|
+
started_at as startedAt, completed_at as completedAt,
|
|
813
|
+
created_at as createdAt, updated_at as updatedAt
|
|
814
|
+
FROM mission_milestones
|
|
815
|
+
WHERE mission_id = ?
|
|
816
|
+
ORDER BY ordering ASC, created_at ASC, rowid ASC
|
|
817
|
+
`).all(input.missionId);
|
|
818
|
+
return rows.map(mapMilestoneRow);
|
|
819
|
+
}
|
|
820
|
+
replaceMissionMilestones(input) {
|
|
821
|
+
this.requireMissionAccess(input);
|
|
822
|
+
const now = Date.now();
|
|
823
|
+
const insert = this.db.transaction(() => {
|
|
824
|
+
this.db.prepare(`DELETE FROM mission_milestones WHERE mission_id = ?`).run(input.missionId);
|
|
825
|
+
for (const milestone of input.milestones) {
|
|
826
|
+
this.db.prepare(`
|
|
827
|
+
INSERT INTO mission_milestones (
|
|
828
|
+
milestone_id, mission_id, name, status, ordering,
|
|
829
|
+
started_at, completed_at, created_at, updated_at
|
|
830
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
831
|
+
`).run(normalizeOptionalString(milestone.milestoneId) ?? randomUUID(), input.missionId, milestone.name, normalizeMilestoneStatus(milestone.status), milestone.ordering, normalizeOptionalNumber(milestone.startedAt), normalizeOptionalNumber(milestone.completedAt), now, now);
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
insert();
|
|
835
|
+
return this.listMissionMilestones({ missionId: input.missionId, userId: input.userId, isAdmin: input.isAdmin });
|
|
836
|
+
}
|
|
837
|
+
updateMissionMilestoneStatus(input) {
|
|
838
|
+
const now = Date.now();
|
|
839
|
+
this.db.prepare(`
|
|
840
|
+
UPDATE mission_milestones
|
|
841
|
+
SET status = ?,
|
|
842
|
+
started_at = COALESCE(?, started_at),
|
|
843
|
+
completed_at = COALESCE(?, completed_at),
|
|
844
|
+
updated_at = ?
|
|
845
|
+
WHERE mission_id = ?
|
|
846
|
+
AND milestone_id = ?
|
|
847
|
+
`).run(input.status, normalizeOptionalNumber(input.startedAt), normalizeOptionalNumber(input.completedAt), now, input.missionId, input.milestoneId);
|
|
848
|
+
const updated = this.listMissionMilestones({ missionId: input.missionId, userId: '', isAdmin: true })
|
|
849
|
+
.find((milestone) => milestone.milestoneId === input.milestoneId);
|
|
850
|
+
if (!updated)
|
|
851
|
+
throw new MissionServiceError(404, 'Milestone not found');
|
|
852
|
+
return updated;
|
|
853
|
+
}
|
|
854
|
+
resetMissionValidationAssertions(input) {
|
|
855
|
+
this.requireMissionAccess(input);
|
|
856
|
+
const now = Date.now();
|
|
857
|
+
if (input.featureId) {
|
|
858
|
+
this.db.prepare(`
|
|
859
|
+
UPDATE mission_validation_assertions
|
|
860
|
+
SET status = 'pending', note = NULL, evidence = NULL, confidence = NULL, updated_at = ?
|
|
861
|
+
WHERE mission_id = ?
|
|
862
|
+
AND feature_id = ?
|
|
863
|
+
`).run(now, input.missionId, input.featureId);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
this.db.prepare(`
|
|
867
|
+
UPDATE mission_validation_assertions
|
|
868
|
+
SET status = 'pending', note = NULL, evidence = NULL, confidence = NULL, updated_at = ?
|
|
869
|
+
WHERE mission_id = ?
|
|
870
|
+
`).run(now, input.missionId);
|
|
871
|
+
}
|
|
872
|
+
return this.listMissionValidationAssertions({ missionId: input.missionId, userId: input.userId, isAdmin: input.isAdmin });
|
|
873
|
+
}
|
|
874
|
+
resetMissionValidationAssertionsByFeatureStatus(input) {
|
|
875
|
+
this.requireMissionAccess(input);
|
|
876
|
+
const now = Date.now();
|
|
877
|
+
this.db.prepare(`
|
|
878
|
+
UPDATE mission_validation_assertions
|
|
879
|
+
SET status = 'pending', note = NULL, evidence = NULL, confidence = NULL, updated_at = ?
|
|
880
|
+
WHERE mission_id = ?
|
|
881
|
+
AND feature_id IN (
|
|
882
|
+
SELECT feature_id FROM mission_features WHERE mission_id = ? AND status = ?
|
|
883
|
+
)
|
|
884
|
+
`).run(now, input.missionId, input.missionId, input.featureStatus);
|
|
885
|
+
return this.listMissionValidationAssertions({ missionId: input.missionId, userId: input.userId, isAdmin: input.isAdmin });
|
|
886
|
+
}
|
|
887
|
+
listEvidence(input) {
|
|
888
|
+
const mission = this.requireMissionAccess(input);
|
|
889
|
+
const events = this.listEvents({
|
|
890
|
+
missionId: input.missionId,
|
|
891
|
+
userId: input.userId,
|
|
892
|
+
isAdmin: input.isAdmin,
|
|
893
|
+
});
|
|
894
|
+
const attempts = this.listAttempts({
|
|
895
|
+
missionId: input.missionId,
|
|
896
|
+
userId: input.userId,
|
|
897
|
+
isAdmin: input.isAdmin,
|
|
898
|
+
});
|
|
899
|
+
const snapshot = this.getLatestSnapshot({
|
|
900
|
+
missionId: input.missionId,
|
|
901
|
+
userId: input.userId,
|
|
902
|
+
isAdmin: input.isAdmin,
|
|
903
|
+
});
|
|
904
|
+
const features = this.listMissionFeatures({
|
|
905
|
+
missionId: input.missionId,
|
|
906
|
+
userId: input.userId,
|
|
907
|
+
isAdmin: input.isAdmin,
|
|
908
|
+
});
|
|
909
|
+
const assertions = this.listMissionValidationAssertions({
|
|
910
|
+
missionId: input.missionId,
|
|
911
|
+
userId: input.userId,
|
|
912
|
+
isAdmin: input.isAdmin,
|
|
913
|
+
});
|
|
914
|
+
const interactions = this.listMissionInteractions({
|
|
915
|
+
missionId: input.missionId,
|
|
916
|
+
userId: input.userId,
|
|
917
|
+
isAdmin: input.isAdmin,
|
|
918
|
+
});
|
|
919
|
+
const evidence = deriveMissionEvidence({
|
|
920
|
+
mission,
|
|
921
|
+
events,
|
|
922
|
+
attempts,
|
|
923
|
+
snapshot,
|
|
924
|
+
features,
|
|
925
|
+
assertions,
|
|
926
|
+
interactions,
|
|
927
|
+
});
|
|
928
|
+
const filter = {
|
|
929
|
+
sourceType: input.sourceType,
|
|
930
|
+
featureId: input.featureId,
|
|
931
|
+
after: input.after,
|
|
932
|
+
limit: input.limit,
|
|
933
|
+
};
|
|
934
|
+
return filterMissionEvidence(evidence, filter);
|
|
935
|
+
}
|
|
936
|
+
listTrace(input) {
|
|
937
|
+
const mission = this.requireMissionAccess(input);
|
|
938
|
+
const events = this.listEvents({
|
|
939
|
+
missionId: input.missionId,
|
|
940
|
+
userId: input.userId,
|
|
941
|
+
isAdmin: input.isAdmin,
|
|
942
|
+
});
|
|
943
|
+
const attempts = this.listAttempts({
|
|
944
|
+
missionId: input.missionId,
|
|
945
|
+
userId: input.userId,
|
|
946
|
+
isAdmin: input.isAdmin,
|
|
947
|
+
});
|
|
948
|
+
const snapshot = this.getLatestSnapshot({
|
|
949
|
+
missionId: input.missionId,
|
|
950
|
+
userId: input.userId,
|
|
951
|
+
isAdmin: input.isAdmin,
|
|
952
|
+
});
|
|
953
|
+
const features = this.listMissionFeatures({
|
|
954
|
+
missionId: input.missionId,
|
|
955
|
+
userId: input.userId,
|
|
956
|
+
isAdmin: input.isAdmin,
|
|
957
|
+
});
|
|
958
|
+
const assertions = this.listMissionValidationAssertions({
|
|
959
|
+
missionId: input.missionId,
|
|
960
|
+
userId: input.userId,
|
|
961
|
+
isAdmin: input.isAdmin,
|
|
962
|
+
});
|
|
963
|
+
const interactions = this.listMissionInteractions({
|
|
964
|
+
missionId: input.missionId,
|
|
965
|
+
userId: input.userId,
|
|
966
|
+
isAdmin: input.isAdmin,
|
|
967
|
+
});
|
|
968
|
+
const traces = deriveMissionTrace({
|
|
969
|
+
mission,
|
|
970
|
+
events,
|
|
971
|
+
attempts,
|
|
972
|
+
snapshot,
|
|
973
|
+
features,
|
|
974
|
+
assertions,
|
|
975
|
+
interactions,
|
|
976
|
+
});
|
|
977
|
+
const filter = {
|
|
978
|
+
role: input.role,
|
|
979
|
+
operation: input.operation,
|
|
980
|
+
confidence: input.confidence,
|
|
981
|
+
after: input.after,
|
|
982
|
+
limit: input.limit,
|
|
983
|
+
};
|
|
984
|
+
return filterMissionTrace(traces, filter);
|
|
985
|
+
}
|
|
986
|
+
markMissionInteractionDelivered(input) {
|
|
987
|
+
const now = Date.now();
|
|
988
|
+
this.db.prepare(`
|
|
989
|
+
UPDATE mission_interactions
|
|
990
|
+
SET status = 'delivered', updated_at = ?
|
|
991
|
+
WHERE mission_id = ?
|
|
992
|
+
AND interaction_id = ?
|
|
993
|
+
`).run(now, input.missionId, input.interactionId);
|
|
994
|
+
const updated = this.getMissionInteractionById(input.interactionId);
|
|
995
|
+
if (!updated)
|
|
996
|
+
throw new MissionServiceError(404, 'Interaction not found');
|
|
997
|
+
this.appendEvent({
|
|
998
|
+
missionId: input.missionId,
|
|
999
|
+
eventType: 'mission_interaction_delivered',
|
|
1000
|
+
source: 'platform',
|
|
1001
|
+
eventTime: now,
|
|
1002
|
+
payload: {
|
|
1003
|
+
interactionId: updated.interactionId,
|
|
1004
|
+
prompt: updated.prompt,
|
|
1005
|
+
position: updated.position,
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
return updated;
|
|
1009
|
+
}
|
|
1010
|
+
updateMissionInteractionStatus(input) {
|
|
1011
|
+
const now = Date.now();
|
|
1012
|
+
this.db.prepare(`
|
|
1013
|
+
UPDATE mission_interactions
|
|
1014
|
+
SET status = ?, updated_at = ?
|
|
1015
|
+
WHERE mission_id = ?
|
|
1016
|
+
AND interaction_id = ?
|
|
1017
|
+
`).run(input.status, now, input.missionId, input.interactionId);
|
|
1018
|
+
const updated = this.getMissionInteractionById(input.interactionId);
|
|
1019
|
+
if (!updated)
|
|
1020
|
+
throw new MissionServiceError(404, 'Interaction not found');
|
|
1021
|
+
return updated;
|
|
1022
|
+
}
|
|
1023
|
+
getMissionInteractionById(interactionId) {
|
|
1024
|
+
const row = this.db.prepare(`
|
|
1025
|
+
SELECT interaction_id as interactionId, mission_id as missionId, prompt, status,
|
|
1026
|
+
position, created_at as createdAt, updated_at as updatedAt
|
|
1027
|
+
FROM mission_interactions
|
|
1028
|
+
WHERE interaction_id = ?
|
|
1029
|
+
`).get(interactionId);
|
|
1030
|
+
return row ? mapInteractionRow(row) : null;
|
|
1031
|
+
}
|
|
1032
|
+
updateMissionValidationAssertion(input) {
|
|
1033
|
+
const now = Date.now();
|
|
1034
|
+
this.db.prepare(`
|
|
1035
|
+
UPDATE mission_validation_assertions
|
|
1036
|
+
SET status = ?, note = ?, evidence = ?, confidence = ?, updated_at = ?
|
|
1037
|
+
WHERE mission_id = ?
|
|
1038
|
+
AND assertion_id = ?
|
|
1039
|
+
`).run(input.status, normalizeOptionalString(input.note), normalizeOptionalString(input.evidence), normalizeMissionTraceConfidence(input.confidence), now, input.missionId, input.assertionId);
|
|
1040
|
+
const updated = this.listMissionValidationAssertions({ missionId: input.missionId, userId: '', isAdmin: true })
|
|
1041
|
+
.find((assertion) => assertion.assertionId === input.assertionId);
|
|
1042
|
+
if (!updated)
|
|
1043
|
+
throw new MissionServiceError(404, 'Assertion not found');
|
|
1044
|
+
return updated;
|
|
1045
|
+
}
|
|
1046
|
+
updateMissionFeatureStatus(input) {
|
|
1047
|
+
const now = Date.now();
|
|
1048
|
+
this.db.prepare(`
|
|
1049
|
+
UPDATE mission_features
|
|
1050
|
+
SET status = ?, updated_at = ?
|
|
1051
|
+
WHERE mission_id = ?
|
|
1052
|
+
AND feature_id = ?
|
|
1053
|
+
`).run(input.status, now, input.missionId, input.featureId);
|
|
1054
|
+
this.appendEvent({
|
|
1055
|
+
missionId: input.missionId,
|
|
1056
|
+
eventType: input.eventType ?? `feature_${input.status}`,
|
|
1057
|
+
source: input.source ?? 'platform',
|
|
1058
|
+
eventTime: now,
|
|
1059
|
+
payload: {
|
|
1060
|
+
featureId: input.featureId,
|
|
1061
|
+
status: input.status,
|
|
1062
|
+
...(input.eventPayload ?? {}),
|
|
1063
|
+
},
|
|
1064
|
+
});
|
|
1065
|
+
const updated = this.listMissionFeatures({ missionId: input.missionId, userId: '', isAdmin: true })
|
|
1066
|
+
.find((feature) => feature.featureId === input.featureId);
|
|
1067
|
+
if (!updated)
|
|
1068
|
+
throw new MissionServiceError(404, 'Feature not found');
|
|
1069
|
+
return updated;
|
|
1070
|
+
}
|
|
1071
|
+
updateMissionFeatureHandoff(input) {
|
|
1072
|
+
const now = Date.now();
|
|
1073
|
+
this.db.prepare(`
|
|
1074
|
+
UPDATE mission_features
|
|
1075
|
+
SET handoff = ?, updated_at = ?
|
|
1076
|
+
WHERE mission_id = ?
|
|
1077
|
+
AND feature_id = ?
|
|
1078
|
+
`).run(JSON.stringify(input.handoff), now, input.missionId, input.featureId);
|
|
1079
|
+
const updated = this.listMissionFeatures({ missionId: input.missionId, userId: '', isAdmin: true })
|
|
1080
|
+
.find((feature) => feature.featureId === input.featureId);
|
|
1081
|
+
if (!updated)
|
|
1082
|
+
throw new MissionServiceError(404, 'Feature not found');
|
|
1083
|
+
return updated;
|
|
1084
|
+
}
|
|
1085
|
+
updateMissionFeatureWorkerOutput(input) {
|
|
1086
|
+
const now = Date.now();
|
|
1087
|
+
this.db.prepare(`
|
|
1088
|
+
UPDATE mission_features
|
|
1089
|
+
SET worker_output = ?, updated_at = ?
|
|
1090
|
+
WHERE mission_id = ?
|
|
1091
|
+
AND feature_id = ?
|
|
1092
|
+
`).run(JSON.stringify(input.workerOutput), now, input.missionId, input.featureId);
|
|
1093
|
+
const updated = this.listMissionFeatures({ missionId: input.missionId, userId: '', isAdmin: true })
|
|
1094
|
+
.find((feature) => feature.featureId === input.featureId);
|
|
1095
|
+
if (!updated)
|
|
1096
|
+
throw new MissionServiceError(404, 'Feature not found');
|
|
1097
|
+
return updated;
|
|
1098
|
+
}
|
|
1099
|
+
incrementMissionFeatureRetryCount(input) {
|
|
1100
|
+
const now = Date.now();
|
|
1101
|
+
this.db.prepare(`
|
|
1102
|
+
UPDATE mission_features
|
|
1103
|
+
SET retry_count = retry_count + 1, updated_at = ?
|
|
1104
|
+
WHERE mission_id = ?
|
|
1105
|
+
AND feature_id = ?
|
|
1106
|
+
`).run(now, input.missionId, input.featureId);
|
|
1107
|
+
const updated = this.listMissionFeatures({ missionId: input.missionId, userId: '', isAdmin: true })
|
|
1108
|
+
.find((feature) => feature.featureId === input.featureId);
|
|
1109
|
+
if (!updated)
|
|
1110
|
+
throw new MissionServiceError(404, 'Feature not found');
|
|
1111
|
+
return updated;
|
|
1112
|
+
}
|
|
1113
|
+
createMissionAttempt(input) {
|
|
1114
|
+
const now = input.createdAt ?? Date.now();
|
|
1115
|
+
const existing = this.getMissionAttemptByRequestId(input.missionId, input.requestId);
|
|
1116
|
+
if (existing) {
|
|
1117
|
+
this.db.prepare(`
|
|
1118
|
+
UPDATE mission_attempts
|
|
1119
|
+
SET node_id = ?,
|
|
1120
|
+
mission_dir = ?,
|
|
1121
|
+
prompt_preview = COALESCE(?, prompt_preview),
|
|
1122
|
+
updated_at = ?
|
|
1123
|
+
WHERE mission_id = ?
|
|
1124
|
+
AND request_id = ?
|
|
1125
|
+
`).run(input.nodeId, input.missionDir, normalizePromptPreview(input.prompt), now, input.missionId, input.requestId);
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
const supersessionReason = normalizeSupersessionReason(input.supersessionReason);
|
|
1129
|
+
if (input.supersedesAttemptId && !supersessionReason) {
|
|
1130
|
+
throw new MissionServiceError(400, 'supersessionReason is required when supersedesAttemptId is provided');
|
|
1131
|
+
}
|
|
1132
|
+
const create = this.db.transaction(() => {
|
|
1133
|
+
this.db.prepare(`
|
|
1134
|
+
INSERT INTO mission_attempts (
|
|
1135
|
+
attempt_id, mission_id, request_id, attempt_type, state, node_id, mission_dir,
|
|
1136
|
+
prompt_preview, created_at, updated_at
|
|
1137
|
+
) VALUES (?, ?, ?, ?, 'queued', ?, ?, ?, ?, ?)
|
|
1138
|
+
`).run(randomUUID(), input.missionId, input.requestId, input.attemptType, input.nodeId, input.missionDir, normalizePromptPreview(input.prompt), now, now);
|
|
1139
|
+
if (input.supersedesAttemptId && supersessionReason) {
|
|
1140
|
+
const prior = this.db.prepare(`
|
|
1141
|
+
SELECT attempt_id as attemptId, superseded_by as supersededBy
|
|
1142
|
+
FROM mission_attempts
|
|
1143
|
+
WHERE attempt_id = ?
|
|
1144
|
+
`).get(input.supersedesAttemptId);
|
|
1145
|
+
if (!prior) {
|
|
1146
|
+
throw new MissionServiceError(404, 'Prior attempt not found');
|
|
1147
|
+
}
|
|
1148
|
+
if (prior.supersededBy) {
|
|
1149
|
+
throw new MissionServiceError(409, 'Prior attempt is already superseded');
|
|
1150
|
+
}
|
|
1151
|
+
const newAttempt = this.db.prepare(`
|
|
1152
|
+
SELECT attempt_id as attemptId
|
|
1153
|
+
FROM mission_attempts
|
|
1154
|
+
WHERE mission_id = ?
|
|
1155
|
+
AND request_id = ?
|
|
1156
|
+
`).get(input.missionId, input.requestId);
|
|
1157
|
+
if (!newAttempt) {
|
|
1158
|
+
throw new MissionServiceError(500, 'Newly created attempt was not found');
|
|
1159
|
+
}
|
|
1160
|
+
this.db.prepare(`
|
|
1161
|
+
UPDATE mission_attempts
|
|
1162
|
+
SET superseded_by = ?,
|
|
1163
|
+
supersession_reason = ?,
|
|
1164
|
+
superseded_at = ?,
|
|
1165
|
+
updated_at = ?
|
|
1166
|
+
WHERE attempt_id = ?
|
|
1167
|
+
`).run(newAttempt.attemptId, supersessionReason, now, now, input.supersedesAttemptId);
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
create();
|
|
1171
|
+
}
|
|
1172
|
+
const attempt = this.getMissionAttemptByRequestId(input.missionId, input.requestId);
|
|
1173
|
+
if (!attempt)
|
|
1174
|
+
throw new MissionServiceError(500, 'Mission attempt was not created.');
|
|
1175
|
+
return attempt;
|
|
1176
|
+
}
|
|
1177
|
+
markAttemptSuperseded(input) {
|
|
1178
|
+
const now = input.supersededAt ?? Date.now();
|
|
1179
|
+
const reason = normalizeSupersessionReason(input.reason);
|
|
1180
|
+
if (!reason) {
|
|
1181
|
+
throw new MissionServiceError(400, "reason must be 'retry', 'recovery', or 'orphan_cleanup'");
|
|
1182
|
+
}
|
|
1183
|
+
const update = this.db.transaction(() => {
|
|
1184
|
+
const prior = this.db.prepare(`
|
|
1185
|
+
SELECT attempt_id as attemptId, mission_id as missionId, superseded_by as supersededBy
|
|
1186
|
+
FROM mission_attempts
|
|
1187
|
+
WHERE attempt_id = ?
|
|
1188
|
+
`).get(input.attemptId);
|
|
1189
|
+
if (!prior) {
|
|
1190
|
+
throw new MissionServiceError(404, 'Attempt not found');
|
|
1191
|
+
}
|
|
1192
|
+
if (prior.supersededBy) {
|
|
1193
|
+
throw new MissionServiceError(409, 'Attempt is already superseded');
|
|
1194
|
+
}
|
|
1195
|
+
const successor = input.supersededBy
|
|
1196
|
+
? this.db.prepare(`
|
|
1197
|
+
SELECT attempt_id as attemptId
|
|
1198
|
+
FROM mission_attempts
|
|
1199
|
+
WHERE attempt_id = ?
|
|
1200
|
+
`).get(input.supersededBy)
|
|
1201
|
+
: undefined;
|
|
1202
|
+
if (input.supersededBy && !successor) {
|
|
1203
|
+
throw new MissionServiceError(404, 'Superseding attempt not found');
|
|
1204
|
+
}
|
|
1205
|
+
this.db.prepare(`
|
|
1206
|
+
UPDATE mission_attempts
|
|
1207
|
+
SET superseded_by = ?,
|
|
1208
|
+
supersession_reason = ?,
|
|
1209
|
+
superseded_at = ?,
|
|
1210
|
+
updated_at = ?
|
|
1211
|
+
WHERE attempt_id = ?
|
|
1212
|
+
`).run(input.supersededBy, reason, now, now, input.attemptId);
|
|
1213
|
+
});
|
|
1214
|
+
update();
|
|
1215
|
+
const attempt = this.db.prepare(`
|
|
1216
|
+
SELECT attempt_id as attemptId, mission_id as missionId, request_id as requestId,
|
|
1217
|
+
attempt_type as attemptType, state, node_id as nodeId, mission_dir as missionDir,
|
|
1218
|
+
prompt_preview as promptPreview, pid, exit_code as exitCode, signal, error,
|
|
1219
|
+
factory_session_id as factorySessionId, factory_artifact_dir as factoryArtifactDir,
|
|
1220
|
+
stdout_event_count as stdoutEventCount, stderr_event_count as stderrEventCount,
|
|
1221
|
+
output_preview as outputPreview, last_output_at as lastOutputAt,
|
|
1222
|
+
superseded_by as supersededBy, supersession_reason as supersessionReason,
|
|
1223
|
+
superseded_at as supersededAt,
|
|
1224
|
+
created_at as createdAt, accepted_at as acceptedAt, completed_at as completedAt,
|
|
1225
|
+
updated_at as updatedAt
|
|
1226
|
+
FROM mission_attempts
|
|
1227
|
+
WHERE attempt_id = ?
|
|
1228
|
+
`).get(input.attemptId);
|
|
1229
|
+
if (!attempt)
|
|
1230
|
+
throw new MissionServiceError(500, 'Attempt was not found after supersession.');
|
|
1231
|
+
return mapAttemptRow(attempt);
|
|
1232
|
+
}
|
|
1233
|
+
getLatestSnapshot(input) {
|
|
1234
|
+
this.requireMissionAccess(input);
|
|
1235
|
+
const row = this.db.prepare(`
|
|
1236
|
+
SELECT snapshot_id as snapshotId, mission_id as missionId, source, snapshot_json as snapshotJson,
|
|
1237
|
+
created_at as createdAt
|
|
1238
|
+
FROM mission_snapshots
|
|
1239
|
+
WHERE mission_id = ?
|
|
1240
|
+
ORDER BY created_at DESC, rowid DESC
|
|
1241
|
+
LIMIT 1
|
|
1242
|
+
`).get(input.missionId);
|
|
1243
|
+
return row ? mapSnapshotRow(row) : null;
|
|
1244
|
+
}
|
|
1245
|
+
importSnapshot(input) {
|
|
1246
|
+
this.requireMissionAccess(input);
|
|
1247
|
+
const now = Date.now();
|
|
1248
|
+
const snapshotId = randomUUID();
|
|
1249
|
+
const source = normalizeOptionalString(input.request.source) ?? 'manual';
|
|
1250
|
+
const snapshot = normalizeSnapshotPayload(input.request.snapshot);
|
|
1251
|
+
const importSnapshot = this.db.transaction(() => {
|
|
1252
|
+
this.db.prepare(`
|
|
1253
|
+
INSERT INTO mission_snapshots (snapshot_id, mission_id, source, snapshot_json, created_at)
|
|
1254
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1255
|
+
`).run(snapshotId, input.missionId, source, JSON.stringify(snapshot), now);
|
|
1256
|
+
this.db.prepare(`
|
|
1257
|
+
UPDATE missions
|
|
1258
|
+
SET last_imported_at = ?, updated_at = ?
|
|
1259
|
+
WHERE mission_id = ?
|
|
1260
|
+
`).run(now, now, input.missionId);
|
|
1261
|
+
this.persistFactoryEvidenceFromPayload(input.missionId, snapshot, now);
|
|
1262
|
+
this.appendEvent({
|
|
1263
|
+
missionId: input.missionId,
|
|
1264
|
+
eventType: 'mission_snapshot_imported',
|
|
1265
|
+
source,
|
|
1266
|
+
eventTime: now,
|
|
1267
|
+
payload: { snapshotId },
|
|
1268
|
+
});
|
|
1269
|
+
});
|
|
1270
|
+
importSnapshot();
|
|
1271
|
+
const row = this.db.prepare(`
|
|
1272
|
+
SELECT snapshot_id as snapshotId, mission_id as missionId, source, snapshot_json as snapshotJson,
|
|
1273
|
+
created_at as createdAt
|
|
1274
|
+
FROM mission_snapshots
|
|
1275
|
+
WHERE snapshot_id = ?
|
|
1276
|
+
`).get(snapshotId);
|
|
1277
|
+
return mapSnapshotRow(row);
|
|
1278
|
+
}
|
|
1279
|
+
importDroidArtifacts(input) {
|
|
1280
|
+
const mission = this.requireMissionAccess(input);
|
|
1281
|
+
const missionDir = mission.missionDir;
|
|
1282
|
+
if (!missionDir)
|
|
1283
|
+
throw new MissionServiceError(400, 'missionDir is required');
|
|
1284
|
+
const trustedFactoryDir = resolveTrustedFactoryArtifactDir(mission.latestFactorySessionId, mission.latestFactoryArtifactDir);
|
|
1285
|
+
const trustedFactorySessionId = trustedFactoryDir ? path.basename(trustedFactoryDir) : null;
|
|
1286
|
+
const importDir = trustedFactoryDir ?? missionDir;
|
|
1287
|
+
let snapshot;
|
|
1288
|
+
try {
|
|
1289
|
+
snapshot = importDroidMissionArtifacts(importDir, Date.now(), {
|
|
1290
|
+
platformMissionDir: trustedFactoryDir ? missionDir : null,
|
|
1291
|
+
factorySessionId: trustedFactorySessionId,
|
|
1292
|
+
fallbackMissionId: mission.missionId,
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
catch (error) {
|
|
1296
|
+
throw new MissionServiceError(400, String(error.message ?? error));
|
|
1297
|
+
}
|
|
1298
|
+
assertDroidSnapshotImportable(snapshot, importDir);
|
|
1299
|
+
return this.importSnapshot({
|
|
1300
|
+
missionId: input.missionId,
|
|
1301
|
+
userId: input.userId,
|
|
1302
|
+
isAdmin: input.isAdmin,
|
|
1303
|
+
request: {
|
|
1304
|
+
source: 'droid_cli',
|
|
1305
|
+
snapshot,
|
|
1306
|
+
},
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
generateNativeMissionSnapshot(input) {
|
|
1310
|
+
const mission = this.requireMissionAccess(input);
|
|
1311
|
+
const features = this.listMissionFeatures({
|
|
1312
|
+
missionId: input.missionId,
|
|
1313
|
+
userId: input.userId,
|
|
1314
|
+
isAdmin: input.isAdmin,
|
|
1315
|
+
});
|
|
1316
|
+
const assertions = this.listMissionValidationAssertions({
|
|
1317
|
+
missionId: input.missionId,
|
|
1318
|
+
userId: input.userId,
|
|
1319
|
+
isAdmin: input.isAdmin,
|
|
1320
|
+
});
|
|
1321
|
+
const events = this.listEvents({
|
|
1322
|
+
missionId: input.missionId,
|
|
1323
|
+
userId: input.userId,
|
|
1324
|
+
isAdmin: input.isAdmin,
|
|
1325
|
+
});
|
|
1326
|
+
const featureItems = features.map((feature) => ({
|
|
1327
|
+
id: feature.featureId,
|
|
1328
|
+
milestone: feature.milestone,
|
|
1329
|
+
description: feature.description,
|
|
1330
|
+
skillName: feature.skillName,
|
|
1331
|
+
status: feature.status,
|
|
1332
|
+
ordering: feature.ordering,
|
|
1333
|
+
preconditions: feature.preconditions,
|
|
1334
|
+
expectedBehavior: feature.expectedBehavior,
|
|
1335
|
+
createdAt: feature.createdAt,
|
|
1336
|
+
updatedAt: feature.updatedAt,
|
|
1337
|
+
}));
|
|
1338
|
+
const handoffItems = features
|
|
1339
|
+
.filter((feature) => feature.handoff != null)
|
|
1340
|
+
.map((feature) => {
|
|
1341
|
+
const handoff = feature.handoff ?? {};
|
|
1342
|
+
return {
|
|
1343
|
+
featureId: feature.featureId,
|
|
1344
|
+
milestone: feature.milestone,
|
|
1345
|
+
status: feature.status,
|
|
1346
|
+
fileName: `${feature.featureId}.json`,
|
|
1347
|
+
timestampMs: feature.updatedAt,
|
|
1348
|
+
salientSummary: typeof handoff.salientSummary === 'string' ? handoff.salientSummary : null,
|
|
1349
|
+
verification: isPlainObject(handoff.verification) ? handoff.verification : null,
|
|
1350
|
+
discoveredIssues: Array.isArray(handoff.discoveredIssues) ? handoff.discoveredIssues : [],
|
|
1351
|
+
raw: handoff,
|
|
1352
|
+
};
|
|
1353
|
+
});
|
|
1354
|
+
const assertionItems = assertions.map((assertion) => ({
|
|
1355
|
+
id: assertion.assertionId,
|
|
1356
|
+
featureId: assertion.featureId,
|
|
1357
|
+
status: assertion.status,
|
|
1358
|
+
note: assertion.note,
|
|
1359
|
+
evidence: assertion.evidence,
|
|
1360
|
+
confidence: assertion.confidence,
|
|
1361
|
+
raw: {
|
|
1362
|
+
assertionId: assertion.assertionId,
|
|
1363
|
+
featureId: assertion.featureId,
|
|
1364
|
+
assertionText: assertion.assertionText,
|
|
1365
|
+
status: assertion.status,
|
|
1366
|
+
note: assertion.note,
|
|
1367
|
+
evidence: assertion.evidence,
|
|
1368
|
+
confidence: assertion.confidence,
|
|
1369
|
+
},
|
|
1370
|
+
}));
|
|
1371
|
+
const timelineRows = events.map((event) => ({
|
|
1372
|
+
eventId: event.eventId,
|
|
1373
|
+
type: event.eventType,
|
|
1374
|
+
source: event.source,
|
|
1375
|
+
timestampMs: event.eventTime,
|
|
1376
|
+
payload: event.payload,
|
|
1377
|
+
}));
|
|
1378
|
+
const statusCounts = countRows(features.map((feature) => feature.status));
|
|
1379
|
+
const assertionStatusCounts = countRows(assertions.map((assertion) => assertion.status));
|
|
1380
|
+
const effectiveConfig = mission.effectiveModelConfig ?? {};
|
|
1381
|
+
const reportEntries = [];
|
|
1382
|
+
if (mission.missionDir) {
|
|
1383
|
+
const reportPath = path.join(mission.missionDir, NATIVE_MISSION_REPORT_FILE_NAME);
|
|
1384
|
+
if (existsSync(reportPath)) {
|
|
1385
|
+
try {
|
|
1386
|
+
const raw = readFileSync(reportPath, 'utf8');
|
|
1387
|
+
const REPORT_TEXT_LIMIT = 120_000;
|
|
1388
|
+
const truncated = raw.length > REPORT_TEXT_LIMIT;
|
|
1389
|
+
reportEntries.push({
|
|
1390
|
+
path: NATIVE_MISSION_REPORT_FILE_NAME,
|
|
1391
|
+
sizeBytes: Buffer.byteLength(raw, 'utf8'),
|
|
1392
|
+
truncated,
|
|
1393
|
+
text: truncated ? `${raw.slice(0, REPORT_TEXT_LIMIT)}\n[truncated]` : raw,
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
catch {
|
|
1397
|
+
// Ignore unreadable report; snapshot should still be generated.
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
const snapshot = {
|
|
1402
|
+
schemaVersion: 1,
|
|
1403
|
+
importedAt: Date.now(),
|
|
1404
|
+
source: 'bigbang_native',
|
|
1405
|
+
missionDir: mission.missionDir,
|
|
1406
|
+
overview: {
|
|
1407
|
+
missionId: mission.missionId,
|
|
1408
|
+
title: mission.title,
|
|
1409
|
+
state: mission.state,
|
|
1410
|
+
runtimeProvider: mission.runtimeProvider,
|
|
1411
|
+
workspacePath: mission.workspacePath,
|
|
1412
|
+
modelMode: mission.modelMode,
|
|
1413
|
+
featureCount: features.length,
|
|
1414
|
+
completedFeatureCount: statusCounts.completed ?? 0,
|
|
1415
|
+
failedFeatureCount: statusCounts.failed ?? 0,
|
|
1416
|
+
validationAssertionCount: assertions.length,
|
|
1417
|
+
handoffCount: handoffItems.length,
|
|
1418
|
+
timelineEventCount: events.length,
|
|
1419
|
+
reportCount: reportEntries.length,
|
|
1420
|
+
},
|
|
1421
|
+
features: {
|
|
1422
|
+
summary: {
|
|
1423
|
+
total: features.length,
|
|
1424
|
+
statusCounts,
|
|
1425
|
+
milestoneCounts: countRows(features.map((feature) => feature.milestone)),
|
|
1426
|
+
},
|
|
1427
|
+
items: featureItems,
|
|
1428
|
+
},
|
|
1429
|
+
timeline: {
|
|
1430
|
+
summary: {
|
|
1431
|
+
total: timelineRows.length,
|
|
1432
|
+
typeCounts: countRows(timelineRows.map((row) => row.type)),
|
|
1433
|
+
},
|
|
1434
|
+
events: timelineRows,
|
|
1435
|
+
},
|
|
1436
|
+
handoffs: {
|
|
1437
|
+
summary: {
|
|
1438
|
+
total: handoffItems.length,
|
|
1439
|
+
milestoneCounts: countRows(handoffItems.map((item) => item.milestone)),
|
|
1440
|
+
statusCounts: countRows(handoffItems.map((item) => item.status)),
|
|
1441
|
+
},
|
|
1442
|
+
items: handoffItems,
|
|
1443
|
+
},
|
|
1444
|
+
validation: {
|
|
1445
|
+
summary: {
|
|
1446
|
+
total: assertions.length,
|
|
1447
|
+
statusCounts: assertionStatusCounts,
|
|
1448
|
+
},
|
|
1449
|
+
assertions: assertionItems,
|
|
1450
|
+
},
|
|
1451
|
+
artifacts: {
|
|
1452
|
+
files: [],
|
|
1453
|
+
reports: reportEntries,
|
|
1454
|
+
},
|
|
1455
|
+
settings: {
|
|
1456
|
+
modelSettings: {
|
|
1457
|
+
orchestratorModel: mission.orchestratorModel,
|
|
1458
|
+
workerModel: mission.workerModel,
|
|
1459
|
+
validatorModel: mission.validatorModel,
|
|
1460
|
+
},
|
|
1461
|
+
runtimeCustomModels: null,
|
|
1462
|
+
validationPolicy: readValidationPolicy(effectiveConfig),
|
|
1463
|
+
validationTimeoutMs: readValidationTimeoutMs(effectiveConfig),
|
|
1464
|
+
},
|
|
1465
|
+
anomalies: [],
|
|
1466
|
+
};
|
|
1467
|
+
const redacted = redactFallbackSecrets(snapshot);
|
|
1468
|
+
return redacted;
|
|
1469
|
+
}
|
|
1470
|
+
generateNativeMissionReport(input) {
|
|
1471
|
+
const mission = this.requireMissionAccess(input);
|
|
1472
|
+
const missionDir = mission.missionDir;
|
|
1473
|
+
if (!missionDir)
|
|
1474
|
+
throw new MissionServiceError(400, 'missionDir is required');
|
|
1475
|
+
const features = this.listMissionFeatures({
|
|
1476
|
+
missionId: input.missionId,
|
|
1477
|
+
userId: input.userId,
|
|
1478
|
+
isAdmin: input.isAdmin,
|
|
1479
|
+
});
|
|
1480
|
+
const assertions = this.listMissionValidationAssertions({
|
|
1481
|
+
missionId: input.missionId,
|
|
1482
|
+
userId: input.userId,
|
|
1483
|
+
isAdmin: input.isAdmin,
|
|
1484
|
+
});
|
|
1485
|
+
const events = this.listEvents({
|
|
1486
|
+
missionId: input.missionId,
|
|
1487
|
+
userId: input.userId,
|
|
1488
|
+
isAdmin: input.isAdmin,
|
|
1489
|
+
});
|
|
1490
|
+
const reportInput = {
|
|
1491
|
+
mission,
|
|
1492
|
+
features,
|
|
1493
|
+
assertions,
|
|
1494
|
+
events,
|
|
1495
|
+
};
|
|
1496
|
+
return generateNativeMissionReport(missionDir, reportInput);
|
|
1497
|
+
}
|
|
1498
|
+
requireMissionAccess(input) {
|
|
1499
|
+
const mission = this.getMission(input);
|
|
1500
|
+
if (!mission)
|
|
1501
|
+
throw new MissionServiceError(404, 'Mission not found');
|
|
1502
|
+
return mission;
|
|
1503
|
+
}
|
|
1504
|
+
appendEvent(input) {
|
|
1505
|
+
const eventId = randomUUID();
|
|
1506
|
+
this.db.prepare(`
|
|
1507
|
+
INSERT INTO mission_events (event_id, mission_id, event_type, source, event_time, payload_json, created_at)
|
|
1508
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1509
|
+
`).run(eventId, input.missionId, input.eventType, input.source, input.eventTime, JSON.stringify(input.payload ?? {}), Date.now());
|
|
1510
|
+
return eventId;
|
|
1511
|
+
}
|
|
1512
|
+
getEventById(eventId) {
|
|
1513
|
+
const row = this.db.prepare(`
|
|
1514
|
+
SELECT event_id as eventId, mission_id as missionId, event_type as eventType,
|
|
1515
|
+
source, event_time as eventTime, payload_json as payloadJson, created_at as createdAt
|
|
1516
|
+
FROM mission_events
|
|
1517
|
+
WHERE event_id = ?
|
|
1518
|
+
`).get(eventId);
|
|
1519
|
+
return row ? mapEventRow(row) : null;
|
|
1520
|
+
}
|
|
1521
|
+
isPhase1PausedPartialBoundaryEligible(mission) {
|
|
1522
|
+
if (mission.state !== 'queued' && mission.state !== 'running')
|
|
1523
|
+
return false;
|
|
1524
|
+
const snapshot = this.getLatestSnapshot({ missionId: mission.missionId, userId: '', isAdmin: true });
|
|
1525
|
+
const events = this.listEvents({ missionId: mission.missionId, userId: '', isAdmin: true });
|
|
1526
|
+
const attempts = this.listAttempts({ missionId: mission.missionId, userId: '', isAdmin: true });
|
|
1527
|
+
if (events.some((event) => isAcceptedPhase1ClosureDecisionEvent(event)))
|
|
1528
|
+
return false;
|
|
1529
|
+
const snapshotData = snapshot?.snapshot ?? {};
|
|
1530
|
+
const observedDroidState = readObservedDroidState(snapshot, events);
|
|
1531
|
+
if (observedDroidState !== 'paused')
|
|
1532
|
+
return false;
|
|
1533
|
+
const hasRunEvidence = attempts.some((attempt) => attempt.attemptType === 'run')
|
|
1534
|
+
|| events.some((event) => event.eventType === 'mission_run_accepted');
|
|
1535
|
+
const hasRuntimeOutput = events.some((event) => event.eventType === 'mission_stdout' || event.eventType === 'mission_stderr' || event.eventType === 'mission_progress' || event.eventType === 'mission_session_event');
|
|
1536
|
+
const hasFactoryEvidence = Boolean(resolveTrustedFactoryArtifactDir(mission.latestFactorySessionId, mission.latestFactoryArtifactDir));
|
|
1537
|
+
const artifactMode = readArtifactMode(snapshotData);
|
|
1538
|
+
const hasArtifactEvidence = artifactMode === 'structured' || artifactMode === 'report_only' || artifactMode === 'runtime_fallback';
|
|
1539
|
+
const hasStructuredOrReportEvidence = artifactMode === 'structured' || artifactMode === 'report_only';
|
|
1540
|
+
const latestInteractionStatus = readLatestInteractionStatus(events);
|
|
1541
|
+
const hasCompletedInteractionEvidence = latestInteractionStatus === 'completed';
|
|
1542
|
+
const latestSessionContinuation = attempts
|
|
1543
|
+
.filter((attempt) => attempt.attemptType === 'session_continuation')
|
|
1544
|
+
.sort((left, right) => right.updatedAt - left.updatedAt)[0];
|
|
1545
|
+
const hasNoFailedContinuation = latestSessionContinuation?.state !== 'failed';
|
|
1546
|
+
const hasTerminalEvidence = attempts.some((attempt) => attempt.attemptType === 'run' && (attempt.state === 'completed' || attempt.state === 'failed'));
|
|
1547
|
+
return hasRunEvidence
|
|
1548
|
+
&& hasRuntimeOutput
|
|
1549
|
+
&& hasFactoryEvidence
|
|
1550
|
+
&& hasArtifactEvidence
|
|
1551
|
+
&& hasStructuredOrReportEvidence
|
|
1552
|
+
&& hasCompletedInteractionEvidence
|
|
1553
|
+
&& hasNoFailedContinuation
|
|
1554
|
+
&& !hasTerminalEvidence;
|
|
1555
|
+
}
|
|
1556
|
+
markAttemptAccepted(input) {
|
|
1557
|
+
const requestId = normalizeOptionalString(input.requestId);
|
|
1558
|
+
if (!requestId)
|
|
1559
|
+
return;
|
|
1560
|
+
this.db.prepare(`
|
|
1561
|
+
UPDATE mission_attempts
|
|
1562
|
+
SET state = 'running',
|
|
1563
|
+
mission_dir = COALESCE(?, mission_dir),
|
|
1564
|
+
pid = COALESCE(?, pid),
|
|
1565
|
+
accepted_at = COALESCE(accepted_at, ?),
|
|
1566
|
+
updated_at = ?
|
|
1567
|
+
WHERE mission_id = ?
|
|
1568
|
+
AND request_id = ?
|
|
1569
|
+
`).run(normalizeOptionalString(input.missionDir), normalizeOptionalNumber(input.pid), input.acceptedAt, input.acceptedAt, input.missionId, requestId);
|
|
1570
|
+
}
|
|
1571
|
+
markAttemptEnded(input) {
|
|
1572
|
+
const requestId = normalizeOptionalString(input.requestId);
|
|
1573
|
+
if (!requestId)
|
|
1574
|
+
return;
|
|
1575
|
+
const exitCode = normalizeOptionalNumber(input.exitCode);
|
|
1576
|
+
const signal = normalizeOptionalString(input.signal);
|
|
1577
|
+
const error = normalizeOptionalString(input.error);
|
|
1578
|
+
const failed = Boolean(error) || (exitCode != null && exitCode !== 0) || Boolean(signal);
|
|
1579
|
+
this.db.prepare(`
|
|
1580
|
+
UPDATE mission_attempts
|
|
1581
|
+
SET state = ?,
|
|
1582
|
+
mission_dir = COALESCE(?, mission_dir),
|
|
1583
|
+
factory_session_id = COALESCE(?, factory_session_id),
|
|
1584
|
+
factory_artifact_dir = COALESCE(?, factory_artifact_dir),
|
|
1585
|
+
exit_code = ?,
|
|
1586
|
+
signal = ?,
|
|
1587
|
+
error = ?,
|
|
1588
|
+
completed_at = ?,
|
|
1589
|
+
updated_at = ?
|
|
1590
|
+
WHERE mission_id = ?
|
|
1591
|
+
AND request_id = ?
|
|
1592
|
+
`).run(failed ? 'failed' : 'completed', normalizeOptionalString(input.missionDir), normalizeOptionalString(input.factorySessionId), normalizeOptionalString(input.factoryArtifactDir), exitCode, signal, error, input.completedAt, input.completedAt, input.missionId, requestId);
|
|
1593
|
+
}
|
|
1594
|
+
markAttemptFailedByRequestId(input) {
|
|
1595
|
+
const requestId = normalizeOptionalString(input.requestId);
|
|
1596
|
+
if (!requestId)
|
|
1597
|
+
return;
|
|
1598
|
+
this.db.prepare(`
|
|
1599
|
+
UPDATE mission_attempts
|
|
1600
|
+
SET state = 'failed',
|
|
1601
|
+
error = ?,
|
|
1602
|
+
completed_at = COALESCE(completed_at, ?),
|
|
1603
|
+
updated_at = ?
|
|
1604
|
+
WHERE mission_id = ?
|
|
1605
|
+
AND request_id = ?
|
|
1606
|
+
`).run(input.error, input.completedAt, input.completedAt, input.missionId, requestId);
|
|
1607
|
+
}
|
|
1608
|
+
recordAttemptOutput(input) {
|
|
1609
|
+
const requestId = normalizeOptionalString(input.requestId);
|
|
1610
|
+
if (!requestId)
|
|
1611
|
+
return;
|
|
1612
|
+
const preview = normalizeOutputPreview(input.stream, input.text);
|
|
1613
|
+
this.db.prepare(`
|
|
1614
|
+
UPDATE mission_attempts
|
|
1615
|
+
SET stdout_event_count = stdout_event_count + ?,
|
|
1616
|
+
stderr_event_count = stderr_event_count + ?,
|
|
1617
|
+
output_preview = COALESCE(?, output_preview),
|
|
1618
|
+
last_output_at = ?,
|
|
1619
|
+
updated_at = ?
|
|
1620
|
+
WHERE mission_id = ?
|
|
1621
|
+
AND request_id = ?
|
|
1622
|
+
`).run(input.stream === 'stdout' ? 1 : 0, input.stream === 'stderr' ? 1 : 0, preview, input.eventTime, input.eventTime, input.missionId, requestId);
|
|
1623
|
+
}
|
|
1624
|
+
getMissionAttemptByRequestId(missionId, requestId) {
|
|
1625
|
+
const row = this.db.prepare(`
|
|
1626
|
+
SELECT attempt_id as attemptId, mission_id as missionId, request_id as requestId,
|
|
1627
|
+
attempt_type as attemptType, state, node_id as nodeId, mission_dir as missionDir,
|
|
1628
|
+
prompt_preview as promptPreview, pid, exit_code as exitCode, signal, error,
|
|
1629
|
+
factory_session_id as factorySessionId, factory_artifact_dir as factoryArtifactDir,
|
|
1630
|
+
stdout_event_count as stdoutEventCount, stderr_event_count as stderrEventCount,
|
|
1631
|
+
output_preview as outputPreview, last_output_at as lastOutputAt,
|
|
1632
|
+
superseded_by as supersededBy, supersession_reason as supersessionReason,
|
|
1633
|
+
superseded_at as supersededAt,
|
|
1634
|
+
created_at as createdAt, accepted_at as acceptedAt, completed_at as completedAt,
|
|
1635
|
+
updated_at as updatedAt
|
|
1636
|
+
FROM mission_attempts
|
|
1637
|
+
WHERE mission_id = ?
|
|
1638
|
+
AND request_id = ?
|
|
1639
|
+
LIMIT 1
|
|
1640
|
+
`).get(missionId, requestId);
|
|
1641
|
+
return row ? mapAttemptRow(row) : null;
|
|
1642
|
+
}
|
|
1643
|
+
persistFactoryEvidenceFromPayload(missionId, payload, seenAt) {
|
|
1644
|
+
if (!isPlainObject(payload))
|
|
1645
|
+
return;
|
|
1646
|
+
const factorySessionId = normalizeOptionalString(payload.factorySessionId);
|
|
1647
|
+
const factoryArtifactDir = normalizeOptionalString(payload.factoryArtifactDir);
|
|
1648
|
+
if (!factorySessionId && !factoryArtifactDir)
|
|
1649
|
+
return;
|
|
1650
|
+
if (factoryArtifactDir && !resolveTrustedFactoryArtifactDir(factorySessionId, factoryArtifactDir))
|
|
1651
|
+
return;
|
|
1652
|
+
this.db.prepare(`
|
|
1653
|
+
UPDATE missions
|
|
1654
|
+
SET latest_factory_session_id = CASE
|
|
1655
|
+
WHEN ? IS NOT NULL THEN COALESCE(?, latest_factory_session_id)
|
|
1656
|
+
WHEN latest_factory_artifact_dir IS NULL THEN COALESCE(?, latest_factory_session_id)
|
|
1657
|
+
ELSE latest_factory_session_id
|
|
1658
|
+
END,
|
|
1659
|
+
latest_factory_artifact_dir = COALESCE(?, latest_factory_artifact_dir),
|
|
1660
|
+
latest_factory_seen_at = CASE
|
|
1661
|
+
WHEN ? IS NOT NULL THEN ?
|
|
1662
|
+
WHEN latest_factory_artifact_dir IS NULL THEN ?
|
|
1663
|
+
ELSE latest_factory_seen_at
|
|
1664
|
+
END
|
|
1665
|
+
WHERE mission_id = ?
|
|
1666
|
+
`).run(factoryArtifactDir, factorySessionId, factorySessionId, factoryArtifactDir, factoryArtifactDir, seenAt, seenAt, missionId);
|
|
1667
|
+
}
|
|
1668
|
+
buildRuntimeFallbackSnapshot(missionId, missionDir, importError) {
|
|
1669
|
+
const mission = this.getMission({ missionId, userId: '', isAdmin: true });
|
|
1670
|
+
const events = this.db.prepare(`
|
|
1671
|
+
SELECT event_id as eventId, mission_id as missionId, event_type as eventType,
|
|
1672
|
+
source, event_time as eventTime, payload_json as payloadJson, created_at as createdAt
|
|
1673
|
+
FROM mission_events
|
|
1674
|
+
WHERE mission_id = ?
|
|
1675
|
+
ORDER BY event_time ASC, created_at ASC, rowid ASC
|
|
1676
|
+
`).all(missionId);
|
|
1677
|
+
const timeline = events.map((row) => mapEventRow(row)).map((event) => ({
|
|
1678
|
+
eventId: event.eventId,
|
|
1679
|
+
type: event.eventType,
|
|
1680
|
+
source: event.source,
|
|
1681
|
+
timestampMs: event.eventTime,
|
|
1682
|
+
payload: redactRuntimePayload(event.payload),
|
|
1683
|
+
}));
|
|
1684
|
+
return {
|
|
1685
|
+
schemaVersion: 1,
|
|
1686
|
+
importedAt: Date.now(),
|
|
1687
|
+
missionDir,
|
|
1688
|
+
overview: {
|
|
1689
|
+
missionId,
|
|
1690
|
+
title: mission?.title ?? null,
|
|
1691
|
+
state: mission?.state ?? 'completed',
|
|
1692
|
+
runtimeProvider: mission?.runtimeProvider ?? 'droid_cli',
|
|
1693
|
+
workspacePath: mission?.workspacePath ?? null,
|
|
1694
|
+
modelMode: mission?.modelMode ?? null,
|
|
1695
|
+
droidArtifactImport: 'failed',
|
|
1696
|
+
runtimeEventCount: timeline.length,
|
|
1697
|
+
},
|
|
1698
|
+
features: { summary: { total: 0 }, items: [] },
|
|
1699
|
+
timeline: {
|
|
1700
|
+
summary: { total: timeline.length, typeCounts: countFallbackEvents(timeline) },
|
|
1701
|
+
events: timeline,
|
|
1702
|
+
},
|
|
1703
|
+
handoffs: { summary: { total: 0 }, items: [] },
|
|
1704
|
+
validation: { summary: { total: 0 }, assertions: [] },
|
|
1705
|
+
artifacts: { files: [] },
|
|
1706
|
+
settings: {
|
|
1707
|
+
modelSettings: {
|
|
1708
|
+
orchestratorModel: mission?.orchestratorModel ?? null,
|
|
1709
|
+
workerModel: mission?.workerModel ?? null,
|
|
1710
|
+
validatorModel: mission?.validatorModel ?? null,
|
|
1711
|
+
},
|
|
1712
|
+
runtimeCustomModels: null,
|
|
1713
|
+
},
|
|
1714
|
+
anomalies: [
|
|
1715
|
+
{
|
|
1716
|
+
severity: 'warning',
|
|
1717
|
+
type: 'missing_droid_artifacts',
|
|
1718
|
+
message: importError,
|
|
1719
|
+
path: missionDir,
|
|
1720
|
+
},
|
|
1721
|
+
],
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function mapMissionRow(row) {
|
|
1726
|
+
return {
|
|
1727
|
+
missionId: row.missionId,
|
|
1728
|
+
ownerUserId: row.ownerUserId,
|
|
1729
|
+
title: row.title,
|
|
1730
|
+
description: row.description,
|
|
1731
|
+
state: normalizeMissionState(row.state),
|
|
1732
|
+
runtimeProvider: normalizeMissionRuntimeProvider(row.runtimeProvider) ?? 'droid_cli',
|
|
1733
|
+
nodeId: row.nodeId,
|
|
1734
|
+
workspacePath: row.workspacePath,
|
|
1735
|
+
missionDir: row.missionDir,
|
|
1736
|
+
initiatorAgentId: row.initiatorAgentId,
|
|
1737
|
+
controlConversationId: row.controlConversationId,
|
|
1738
|
+
modelMode: normalizeModelMode(row.modelMode),
|
|
1739
|
+
orchestratorModel: row.orchestratorModel,
|
|
1740
|
+
workerModel: row.workerModel,
|
|
1741
|
+
validatorModel: row.validatorModel,
|
|
1742
|
+
effectiveModelConfig: parseJsonObject(row.effectiveModelConfigJson),
|
|
1743
|
+
createdAt: row.createdAt,
|
|
1744
|
+
updatedAt: row.updatedAt,
|
|
1745
|
+
startedAt: row.startedAt,
|
|
1746
|
+
completedAt: row.completedAt,
|
|
1747
|
+
lastImportedAt: row.lastImportedAt,
|
|
1748
|
+
latestFactorySessionId: row.latestFactorySessionId,
|
|
1749
|
+
latestFactoryArtifactDir: row.latestFactoryArtifactDir,
|
|
1750
|
+
latestFactorySeenAt: row.latestFactorySeenAt,
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
function mapSnapshotRow(row) {
|
|
1754
|
+
return {
|
|
1755
|
+
snapshotId: row.snapshotId,
|
|
1756
|
+
missionId: row.missionId,
|
|
1757
|
+
source: row.source,
|
|
1758
|
+
snapshot: parseJsonObject(row.snapshotJson) ?? {},
|
|
1759
|
+
createdAt: row.createdAt,
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
function mapEventRow(row) {
|
|
1763
|
+
return {
|
|
1764
|
+
eventId: row.eventId,
|
|
1765
|
+
missionId: row.missionId,
|
|
1766
|
+
eventType: row.eventType,
|
|
1767
|
+
source: row.source,
|
|
1768
|
+
eventTime: row.eventTime,
|
|
1769
|
+
payload: parseJsonObject(row.payloadJson) ?? {},
|
|
1770
|
+
createdAt: row.createdAt,
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
function mapAttemptRow(row) {
|
|
1774
|
+
return {
|
|
1775
|
+
attemptId: row.attemptId,
|
|
1776
|
+
missionId: row.missionId,
|
|
1777
|
+
requestId: row.requestId,
|
|
1778
|
+
attemptType: normalizeAttemptType(row.attemptType),
|
|
1779
|
+
state: normalizeAttemptState(row.state),
|
|
1780
|
+
nodeId: row.nodeId,
|
|
1781
|
+
missionDir: row.missionDir,
|
|
1782
|
+
promptPreview: row.promptPreview,
|
|
1783
|
+
pid: row.pid,
|
|
1784
|
+
exitCode: row.exitCode,
|
|
1785
|
+
signal: row.signal,
|
|
1786
|
+
error: row.error,
|
|
1787
|
+
factorySessionId: row.factorySessionId,
|
|
1788
|
+
factoryArtifactDir: row.factoryArtifactDir,
|
|
1789
|
+
stdoutEventCount: row.stdoutEventCount,
|
|
1790
|
+
stderrEventCount: row.stderrEventCount,
|
|
1791
|
+
outputPreview: row.outputPreview,
|
|
1792
|
+
lastOutputAt: row.lastOutputAt,
|
|
1793
|
+
supersededBy: row.supersededBy ?? null,
|
|
1794
|
+
supersessionReason: normalizeSupersessionReason(row.supersessionReason),
|
|
1795
|
+
supersededAt: row.supersededAt ?? null,
|
|
1796
|
+
createdAt: row.createdAt,
|
|
1797
|
+
acceptedAt: row.acceptedAt,
|
|
1798
|
+
completedAt: row.completedAt,
|
|
1799
|
+
updatedAt: row.updatedAt,
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
function mapFeatureRow(row) {
|
|
1803
|
+
return {
|
|
1804
|
+
featureId: row.featureId,
|
|
1805
|
+
missionId: row.missionId,
|
|
1806
|
+
milestone: row.milestone,
|
|
1807
|
+
description: row.description,
|
|
1808
|
+
skillName: row.skillName,
|
|
1809
|
+
status: normalizeNativeFeatureStatus(row.status),
|
|
1810
|
+
ordering: row.ordering,
|
|
1811
|
+
preconditions: parseJsonStringArray(row.preconditionsJson),
|
|
1812
|
+
expectedBehavior: parseJsonStringArray(row.expectedBehaviorJson),
|
|
1813
|
+
retryCount: typeof row.retryCount === 'number' ? Math.max(0, row.retryCount) : 0,
|
|
1814
|
+
isValidator: row.isValidator === 1,
|
|
1815
|
+
handoff: parseJsonObject(row.handoffJson),
|
|
1816
|
+
workerOutput: parseJsonObject(row.workerOutputJson),
|
|
1817
|
+
createdAt: row.createdAt,
|
|
1818
|
+
updatedAt: row.updatedAt,
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
function normalizeNativeFeatureStatus(value) {
|
|
1822
|
+
if (value === 'pending'
|
|
1823
|
+
|| value === 'queued'
|
|
1824
|
+
|| value === 'running'
|
|
1825
|
+
|| value === 'completed'
|
|
1826
|
+
|| value === 'failed'
|
|
1827
|
+
|| value === 'skipped'
|
|
1828
|
+
|| value === 'cancelled') {
|
|
1829
|
+
return value;
|
|
1830
|
+
}
|
|
1831
|
+
return 'pending';
|
|
1832
|
+
}
|
|
1833
|
+
function mapAssertionRow(row) {
|
|
1834
|
+
return {
|
|
1835
|
+
assertionId: row.assertionId,
|
|
1836
|
+
missionId: row.missionId,
|
|
1837
|
+
featureId: row.featureId,
|
|
1838
|
+
assertionText: row.assertionText,
|
|
1839
|
+
status: normalizeNativeAssertionStatus(row.status),
|
|
1840
|
+
note: row.note,
|
|
1841
|
+
evidence: row.evidence,
|
|
1842
|
+
confidence: normalizeMissionTraceConfidence(row.confidence),
|
|
1843
|
+
createdAt: row.createdAt,
|
|
1844
|
+
updatedAt: row.updatedAt,
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
function normalizeMissionTraceConfidence(value) {
|
|
1848
|
+
if (value === 'observed' || value === 'derived' || value === 'inferred' || value === 'unknown') {
|
|
1849
|
+
return value;
|
|
1850
|
+
}
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
function normalizeNativeAssertionStatus(value) {
|
|
1854
|
+
if (value === 'pending' || value === 'passed' || value === 'failed' || value === 'blocked') {
|
|
1855
|
+
return value;
|
|
1856
|
+
}
|
|
1857
|
+
return 'pending';
|
|
1858
|
+
}
|
|
1859
|
+
function mapInteractionRow(row) {
|
|
1860
|
+
return {
|
|
1861
|
+
interactionId: row.interactionId,
|
|
1862
|
+
missionId: row.missionId,
|
|
1863
|
+
prompt: row.prompt,
|
|
1864
|
+
status: normalizeMissionInteractionStatus(row.status),
|
|
1865
|
+
position: row.position,
|
|
1866
|
+
createdAt: row.createdAt,
|
|
1867
|
+
updatedAt: row.updatedAt,
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
function mapPlanRow(row) {
|
|
1871
|
+
return {
|
|
1872
|
+
missionId: row.missionId,
|
|
1873
|
+
plan: parseJsonObject(row.planJson) ?? {},
|
|
1874
|
+
version: row.version,
|
|
1875
|
+
createdAt: row.createdAt,
|
|
1876
|
+
updatedAt: row.updatedAt,
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
function mapMilestoneRow(row) {
|
|
1880
|
+
return {
|
|
1881
|
+
milestoneId: row.milestoneId,
|
|
1882
|
+
missionId: row.missionId,
|
|
1883
|
+
name: row.name,
|
|
1884
|
+
status: normalizeMilestoneStatus(row.status),
|
|
1885
|
+
ordering: row.ordering,
|
|
1886
|
+
startedAt: row.startedAt,
|
|
1887
|
+
completedAt: row.completedAt,
|
|
1888
|
+
createdAt: row.createdAt,
|
|
1889
|
+
updatedAt: row.updatedAt,
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
function normalizeMilestoneStatus(value) {
|
|
1893
|
+
if (value === 'pending'
|
|
1894
|
+
|| value === 'running'
|
|
1895
|
+
|| value === 'completed'
|
|
1896
|
+
|| value === 'failed'
|
|
1897
|
+
|| value === 'skipped') {
|
|
1898
|
+
return value;
|
|
1899
|
+
}
|
|
1900
|
+
return 'pending';
|
|
1901
|
+
}
|
|
1902
|
+
function normalizeMissionInteractionStatus(value) {
|
|
1903
|
+
if (value === 'queued' || value === 'delivered' || value === 'observed' || value === 'applied' || value === 'completed' || value === 'failed') {
|
|
1904
|
+
return value;
|
|
1905
|
+
}
|
|
1906
|
+
return 'queued';
|
|
1907
|
+
}
|
|
1908
|
+
function parseJsonStringArray(value) {
|
|
1909
|
+
if (!value)
|
|
1910
|
+
return [];
|
|
1911
|
+
try {
|
|
1912
|
+
const parsed = JSON.parse(value);
|
|
1913
|
+
return Array.isArray(parsed) && parsed.every((item) => typeof item === 'string')
|
|
1914
|
+
? parsed
|
|
1915
|
+
: [];
|
|
1916
|
+
}
|
|
1917
|
+
catch {
|
|
1918
|
+
return [];
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
function countRows(values) {
|
|
1922
|
+
const counts = {};
|
|
1923
|
+
for (const value of values) {
|
|
1924
|
+
const key = value || 'unknown';
|
|
1925
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
1926
|
+
}
|
|
1927
|
+
return counts;
|
|
1928
|
+
}
|
|
1929
|
+
function normalizeMissionState(value) {
|
|
1930
|
+
return ['draft', 'queued', 'running', 'paused', 'completed', 'failed', 'cancelled'].includes(value)
|
|
1931
|
+
? value
|
|
1932
|
+
: 'draft';
|
|
1933
|
+
}
|
|
1934
|
+
function normalizeAttemptType(value) {
|
|
1935
|
+
return ['run', 'session_continuation', 'active_interaction'].includes(value)
|
|
1936
|
+
? value
|
|
1937
|
+
: 'run';
|
|
1938
|
+
}
|
|
1939
|
+
function normalizeAttemptState(value) {
|
|
1940
|
+
return ['queued', 'running', 'completed', 'failed'].includes(value)
|
|
1941
|
+
? value
|
|
1942
|
+
: 'queued';
|
|
1943
|
+
}
|
|
1944
|
+
function normalizeSupersessionReason(value) {
|
|
1945
|
+
if (value === 'retry' || value === 'recovery' || value === 'orphan_cleanup')
|
|
1946
|
+
return value;
|
|
1947
|
+
return null;
|
|
1948
|
+
}
|
|
1949
|
+
function normalizeModelMode(value) {
|
|
1950
|
+
return value === 'droid_default' ? 'droid_default' : 'platform_override';
|
|
1951
|
+
}
|
|
1952
|
+
function normalizeMissionRuntimeProvider(value) {
|
|
1953
|
+
return value === 'droid_cli' || value === 'bigbang_native' ? value : null;
|
|
1954
|
+
}
|
|
1955
|
+
function normalizeMissionRuntimeProviderForUpdate(value, current) {
|
|
1956
|
+
const normalized = normalizeMissionRuntimeProvider(value);
|
|
1957
|
+
if (!normalized) {
|
|
1958
|
+
throw new MissionServiceError(400, "runtimeProvider must be 'droid_cli' or 'bigbang_native'");
|
|
1959
|
+
}
|
|
1960
|
+
if (normalized !== current.runtimeProvider && isMissionRuntimeProviderImmutable(current.state)) {
|
|
1961
|
+
throw new MissionServiceError(409, 'Runtime provider cannot be changed for terminal missions');
|
|
1962
|
+
}
|
|
1963
|
+
return normalized;
|
|
1964
|
+
}
|
|
1965
|
+
function isMissionRuntimeProviderImmutable(state) {
|
|
1966
|
+
return state === 'failed' || state === 'completed' || state === 'paused' || state === 'cancelled';
|
|
1967
|
+
}
|
|
1968
|
+
function normalizeRequiredString(value, field) {
|
|
1969
|
+
const normalized = normalizeOptionalString(value);
|
|
1970
|
+
if (!normalized)
|
|
1971
|
+
throw new MissionServiceError(400, `${field} is required`);
|
|
1972
|
+
return normalized;
|
|
1973
|
+
}
|
|
1974
|
+
function normalizeOptionalString(value) {
|
|
1975
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
1976
|
+
}
|
|
1977
|
+
function normalizeOptionalNumber(value) {
|
|
1978
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
1979
|
+
}
|
|
1980
|
+
function normalizePromptPreview(value) {
|
|
1981
|
+
const normalized = normalizeOptionalString(value);
|
|
1982
|
+
if (!normalized)
|
|
1983
|
+
return null;
|
|
1984
|
+
return normalized.length <= 2_000 ? normalized : `${normalized.slice(0, 2_000)}...`;
|
|
1985
|
+
}
|
|
1986
|
+
function normalizeOutputPreview(stream, value) {
|
|
1987
|
+
const normalized = normalizeOptionalString(value);
|
|
1988
|
+
if (!normalized)
|
|
1989
|
+
return null;
|
|
1990
|
+
const collapsed = normalized.replace(/\s+/g, ' ').trim();
|
|
1991
|
+
if (!collapsed)
|
|
1992
|
+
return null;
|
|
1993
|
+
const tagged = `${stream}: ${collapsed}`;
|
|
1994
|
+
return tagged.length <= 2_000 ? tagged : `...${tagged.slice(-2_000)}`;
|
|
1995
|
+
}
|
|
1996
|
+
function asPayloadString(payload, key) {
|
|
1997
|
+
return normalizeOptionalString(payload[key]);
|
|
1998
|
+
}
|
|
1999
|
+
function asPayloadNumber(payload, key) {
|
|
2000
|
+
return normalizeOptionalNumber(payload[key]);
|
|
2001
|
+
}
|
|
2002
|
+
function hasOwn(value, key) {
|
|
2003
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
2004
|
+
}
|
|
2005
|
+
function nextModelValue(request, key, current, modelMode) {
|
|
2006
|
+
if (modelMode === 'droid_default')
|
|
2007
|
+
return null;
|
|
2008
|
+
if (hasOwn(request, key))
|
|
2009
|
+
return normalizeOptionalString(request[key]) ?? DEFAULT_DROID_MODEL;
|
|
2010
|
+
return current ?? DEFAULT_DROID_MODEL;
|
|
2011
|
+
}
|
|
2012
|
+
function collectChangedMissionFields(current, next) {
|
|
2013
|
+
const scalarFields = [
|
|
2014
|
+
'title',
|
|
2015
|
+
'description',
|
|
2016
|
+
'runtimeProvider',
|
|
2017
|
+
'nodeId',
|
|
2018
|
+
'workspacePath',
|
|
2019
|
+
'missionDir',
|
|
2020
|
+
'initiatorAgentId',
|
|
2021
|
+
'controlConversationId',
|
|
2022
|
+
'modelMode',
|
|
2023
|
+
'orchestratorModel',
|
|
2024
|
+
'workerModel',
|
|
2025
|
+
'validatorModel',
|
|
2026
|
+
];
|
|
2027
|
+
const changed = scalarFields.filter((field) => current[field] !== next[field]);
|
|
2028
|
+
if (next.validationPolicy !== readValidationPolicy(current.effectiveModelConfig)) {
|
|
2029
|
+
changed.push('validationPolicy');
|
|
2030
|
+
}
|
|
2031
|
+
if (next.validationTimeoutMs !== readValidationTimeoutMs(current.effectiveModelConfig)) {
|
|
2032
|
+
changed.push('validationTimeoutMs');
|
|
2033
|
+
}
|
|
2034
|
+
if (JSON.stringify(next.retryPolicy) !== JSON.stringify(readRetryPolicy(current.effectiveModelConfig))) {
|
|
2035
|
+
changed.push('retryPolicy');
|
|
2036
|
+
}
|
|
2037
|
+
return changed.sort();
|
|
2038
|
+
}
|
|
2039
|
+
function normalizeValidationPolicy(value) {
|
|
2040
|
+
if (value === 'failOnFirst' || value === 'continueOnWarning')
|
|
2041
|
+
return value;
|
|
2042
|
+
if (value === null || value === undefined)
|
|
2043
|
+
return null;
|
|
2044
|
+
throw new MissionServiceError(400, "validationPolicy must be 'failOnFirst' or 'continueOnWarning'");
|
|
2045
|
+
}
|
|
2046
|
+
function normalizeValidationTimeoutMs(value) {
|
|
2047
|
+
if (value === null || value === undefined)
|
|
2048
|
+
return null;
|
|
2049
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0)
|
|
2050
|
+
return Math.floor(value);
|
|
2051
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
2052
|
+
const parsed = Number(value);
|
|
2053
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
2054
|
+
return Math.floor(parsed);
|
|
2055
|
+
}
|
|
2056
|
+
throw new MissionServiceError(400, 'validationTimeoutMs must be a positive number');
|
|
2057
|
+
}
|
|
2058
|
+
function readValidationPolicy(config) {
|
|
2059
|
+
const value = config?.validationPolicy;
|
|
2060
|
+
if (value === 'failOnFirst' || value === 'continueOnWarning')
|
|
2061
|
+
return value;
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
function readValidationTimeoutMs(config) {
|
|
2065
|
+
const value = config?.validationTimeoutMs;
|
|
2066
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0)
|
|
2067
|
+
return Math.floor(value);
|
|
2068
|
+
return null;
|
|
2069
|
+
}
|
|
2070
|
+
function normalizeRetryBackoff(value) {
|
|
2071
|
+
if (value === 'exponential')
|
|
2072
|
+
return 'exponential';
|
|
2073
|
+
return 'fixed';
|
|
2074
|
+
}
|
|
2075
|
+
function normalizeRetryPolicy(value) {
|
|
2076
|
+
if (value === null || value === undefined)
|
|
2077
|
+
return null;
|
|
2078
|
+
if (!isPlainObject(value)) {
|
|
2079
|
+
throw new MissionServiceError(400, 'retryPolicy must be an object');
|
|
2080
|
+
}
|
|
2081
|
+
const obj = value;
|
|
2082
|
+
let maxRetries;
|
|
2083
|
+
const maxRetriesRaw = obj.maxRetries;
|
|
2084
|
+
if (typeof maxRetriesRaw === 'number' && Number.isFinite(maxRetriesRaw)) {
|
|
2085
|
+
maxRetries = Math.max(0, Math.floor(maxRetriesRaw));
|
|
2086
|
+
}
|
|
2087
|
+
else if (typeof maxRetriesRaw === 'string' && maxRetriesRaw.trim() !== '') {
|
|
2088
|
+
const parsed = Number(maxRetriesRaw);
|
|
2089
|
+
if (Number.isFinite(parsed)) {
|
|
2090
|
+
maxRetries = Math.max(0, Math.floor(parsed));
|
|
2091
|
+
}
|
|
2092
|
+
else {
|
|
2093
|
+
throw new MissionServiceError(400, 'retryPolicy.maxRetries must be a non-negative number');
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
else if (maxRetriesRaw === null || maxRetriesRaw === undefined) {
|
|
2097
|
+
maxRetries = 0;
|
|
2098
|
+
}
|
|
2099
|
+
else {
|
|
2100
|
+
throw new MissionServiceError(400, 'retryPolicy.maxRetries must be a non-negative number');
|
|
2101
|
+
}
|
|
2102
|
+
const backoff = normalizeRetryBackoff(obj.backoff);
|
|
2103
|
+
let baseDelayMs;
|
|
2104
|
+
if (typeof obj.baseDelayMs === 'number' && Number.isFinite(obj.baseDelayMs) && obj.baseDelayMs >= 0) {
|
|
2105
|
+
baseDelayMs = Math.floor(obj.baseDelayMs);
|
|
2106
|
+
}
|
|
2107
|
+
else if (typeof obj.baseDelayMs === 'string' && obj.baseDelayMs.trim() !== '') {
|
|
2108
|
+
const parsed = Number(obj.baseDelayMs);
|
|
2109
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
2110
|
+
baseDelayMs = Math.floor(parsed);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
let maxDelayMs;
|
|
2114
|
+
if (typeof obj.maxDelayMs === 'number' && Number.isFinite(obj.maxDelayMs) && obj.maxDelayMs >= 0) {
|
|
2115
|
+
maxDelayMs = Math.floor(obj.maxDelayMs);
|
|
2116
|
+
}
|
|
2117
|
+
else if (typeof obj.maxDelayMs === 'string' && obj.maxDelayMs.trim() !== '') {
|
|
2118
|
+
const parsed = Number(obj.maxDelayMs);
|
|
2119
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
2120
|
+
maxDelayMs = Math.floor(parsed);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
const policy = { maxRetries, backoff };
|
|
2124
|
+
if (typeof baseDelayMs === 'number')
|
|
2125
|
+
policy.baseDelayMs = baseDelayMs;
|
|
2126
|
+
if (typeof maxDelayMs === 'number')
|
|
2127
|
+
policy.maxDelayMs = maxDelayMs;
|
|
2128
|
+
return policy;
|
|
2129
|
+
}
|
|
2130
|
+
function readRetryPolicy(config) {
|
|
2131
|
+
const value = config?.retryPolicy;
|
|
2132
|
+
if (!isPlainObject(value))
|
|
2133
|
+
return DEFAULT_RETRY_POLICY;
|
|
2134
|
+
const obj = value;
|
|
2135
|
+
const maxRetriesRaw = obj.maxRetries;
|
|
2136
|
+
const maxRetries = typeof maxRetriesRaw === 'number' && Number.isFinite(maxRetriesRaw)
|
|
2137
|
+
? Math.max(0, Math.floor(maxRetriesRaw))
|
|
2138
|
+
: 0;
|
|
2139
|
+
const backoff = normalizeRetryBackoff(obj.backoff);
|
|
2140
|
+
const policy = { maxRetries, backoff };
|
|
2141
|
+
if (typeof obj.baseDelayMs === 'number' && Number.isFinite(obj.baseDelayMs) && obj.baseDelayMs >= 0) {
|
|
2142
|
+
policy.baseDelayMs = Math.floor(obj.baseDelayMs);
|
|
2143
|
+
}
|
|
2144
|
+
if (typeof obj.maxDelayMs === 'number' && Number.isFinite(obj.maxDelayMs) && obj.maxDelayMs >= 0) {
|
|
2145
|
+
policy.maxDelayMs = Math.floor(obj.maxDelayMs);
|
|
2146
|
+
}
|
|
2147
|
+
return policy;
|
|
2148
|
+
}
|
|
2149
|
+
function buildEffectiveModelConfig(input) {
|
|
2150
|
+
const config = { mode: input.mode };
|
|
2151
|
+
if (input.validationPolicy) {
|
|
2152
|
+
config.validationPolicy = input.validationPolicy;
|
|
2153
|
+
}
|
|
2154
|
+
if (typeof input.validationTimeoutMs === 'number') {
|
|
2155
|
+
config.validationTimeoutMs = input.validationTimeoutMs;
|
|
2156
|
+
}
|
|
2157
|
+
const retryPolicy = input.retryPolicy ?? DEFAULT_RETRY_POLICY;
|
|
2158
|
+
if (retryPolicy.maxRetries > 0 || retryPolicy.backoff !== 'fixed') {
|
|
2159
|
+
config.retryPolicy = retryPolicy;
|
|
2160
|
+
}
|
|
2161
|
+
return config;
|
|
2162
|
+
}
|
|
2163
|
+
function redactRuntimePayload(payload) {
|
|
2164
|
+
return redactFallbackSecrets(payload);
|
|
2165
|
+
}
|
|
2166
|
+
function redactFallbackSecrets(value) {
|
|
2167
|
+
if (Array.isArray(value))
|
|
2168
|
+
return value.map(redactFallbackSecrets);
|
|
2169
|
+
if (isPlainObject(value)) {
|
|
2170
|
+
const out = {};
|
|
2171
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2172
|
+
out[key] = isSecretLikeKey(key) ? '[redacted]' : redactFallbackSecrets(item);
|
|
2173
|
+
}
|
|
2174
|
+
return out;
|
|
2175
|
+
}
|
|
2176
|
+
if (typeof value === 'string')
|
|
2177
|
+
return redactSecretLikeText(value);
|
|
2178
|
+
return value;
|
|
2179
|
+
}
|
|
2180
|
+
function isSecretLikeKey(key) {
|
|
2181
|
+
return /(api[_-]?key|secret|token|password|authorization|credential|bearer)/i.test(key);
|
|
2182
|
+
}
|
|
2183
|
+
function redactSecretLikeText(value) {
|
|
2184
|
+
return value
|
|
2185
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, 'Bearer [redacted]')
|
|
2186
|
+
.replace(/\b(?:sk|sk-ant|sk-proj|ghp|github_pat|xox[baprs])[-_A-Za-z0-9]{8,}\b/g, '[redacted]');
|
|
2187
|
+
}
|
|
2188
|
+
function countFallbackEvents(events) {
|
|
2189
|
+
const counts = {};
|
|
2190
|
+
for (const event of events) {
|
|
2191
|
+
const type = typeof event.type === 'string' && event.type ? event.type : 'unknown';
|
|
2192
|
+
counts[type] = (counts[type] ?? 0) + 1;
|
|
2193
|
+
}
|
|
2194
|
+
return counts;
|
|
2195
|
+
}
|
|
2196
|
+
function normalizeSnapshotPayload(value) {
|
|
2197
|
+
if (isPlainObject(value))
|
|
2198
|
+
return value;
|
|
2199
|
+
throw new MissionServiceError(400, 'snapshot must be an object');
|
|
2200
|
+
}
|
|
2201
|
+
export function resolveTrustedFactoryArtifactDir(factorySessionId, factoryArtifactDir) {
|
|
2202
|
+
const sessionId = normalizeOptionalString(factorySessionId);
|
|
2203
|
+
const artifactDir = normalizeOptionalString(factoryArtifactDir);
|
|
2204
|
+
if (!sessionId || !artifactDir)
|
|
2205
|
+
return null;
|
|
2206
|
+
if (!isSafeFactorySessionId(sessionId))
|
|
2207
|
+
return null;
|
|
2208
|
+
const resolved = path.resolve(artifactDir);
|
|
2209
|
+
const factoryMissionsRoot = path.resolve(homedir(), '.factory', 'missions');
|
|
2210
|
+
const artifactSessionId = path.basename(resolved);
|
|
2211
|
+
if (path.dirname(resolved) !== factoryMissionsRoot)
|
|
2212
|
+
return null;
|
|
2213
|
+
if (artifactSessionId !== sessionId)
|
|
2214
|
+
return null;
|
|
2215
|
+
return resolved;
|
|
2216
|
+
}
|
|
2217
|
+
function isSafeFactorySessionId(value) {
|
|
2218
|
+
return /^[A-Za-z0-9._-]+$/.test(value) && /[A-Za-z0-9_-]/.test(value);
|
|
2219
|
+
}
|
|
2220
|
+
function isPlainObject(value) {
|
|
2221
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
2222
|
+
return false;
|
|
2223
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2224
|
+
return prototype === Object.prototype || prototype === null;
|
|
2225
|
+
}
|
|
2226
|
+
function parseJsonObject(value) {
|
|
2227
|
+
if (!value)
|
|
2228
|
+
return null;
|
|
2229
|
+
try {
|
|
2230
|
+
const parsed = JSON.parse(value);
|
|
2231
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
2232
|
+
? parsed
|
|
2233
|
+
: null;
|
|
2234
|
+
}
|
|
2235
|
+
catch {
|
|
2236
|
+
return null;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
function isAcceptedPhase1ClosureDecisionEvent(event) {
|
|
2240
|
+
const payload = event.payload ?? {};
|
|
2241
|
+
return event.eventType === 'mission_phase1_closure_decision'
|
|
2242
|
+
&& normalizeOptionalString(payload.phase) === 'phase1'
|
|
2243
|
+
&& normalizeOptionalString(payload.decision) === 'accepted_paused_partial_boundary';
|
|
2244
|
+
}
|
|
2245
|
+
function readObservedDroidState(snapshot, events) {
|
|
2246
|
+
const snapshotData = snapshot?.snapshot ?? {};
|
|
2247
|
+
const overviewState = normalizeOptionalString(asRecordValue(snapshotData.overview).state);
|
|
2248
|
+
const snapshotTime = snapshot?.createdAt ?? null;
|
|
2249
|
+
let latest = overviewState
|
|
2250
|
+
? { state: overviewState, eventTime: snapshotTime }
|
|
2251
|
+
: { state: null, eventTime: null };
|
|
2252
|
+
const timelineEvents = asRecordArray(asRecordValue(snapshotData.timeline).events);
|
|
2253
|
+
const pausedTimeline = timelineEvents.find((event) => normalizeOptionalString(event.type)?.includes('paused'));
|
|
2254
|
+
if (pausedTimeline && latest.state == null) {
|
|
2255
|
+
latest = { state: 'paused', eventTime: snapshotTime };
|
|
2256
|
+
}
|
|
2257
|
+
for (const event of events) {
|
|
2258
|
+
const state = readObservedDroidStateFromEvent(event);
|
|
2259
|
+
if (!state)
|
|
2260
|
+
continue;
|
|
2261
|
+
if (latest.eventTime == null || event.eventTime >= latest.eventTime) {
|
|
2262
|
+
latest = { state, eventTime: event.eventTime };
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
return latest.state;
|
|
2266
|
+
}
|
|
2267
|
+
function readObservedDroidStateFromEvent(event) {
|
|
2268
|
+
if (event.eventType === 'mission_run_accepted')
|
|
2269
|
+
return 'running';
|
|
2270
|
+
if (event.eventType === 'mission_completed')
|
|
2271
|
+
return 'completed';
|
|
2272
|
+
if (event.eventType === 'mission_failed')
|
|
2273
|
+
return 'failed';
|
|
2274
|
+
if (event.eventType !== 'mission_progress')
|
|
2275
|
+
return null;
|
|
2276
|
+
const payload = asRecordValue(event.payload);
|
|
2277
|
+
const type = normalizeOptionalString(payload.type) ?? normalizeOptionalString(asRecordValue(payload.droidEvent).type);
|
|
2278
|
+
if (type?.includes('paused'))
|
|
2279
|
+
return 'paused';
|
|
2280
|
+
if (type?.includes('completed'))
|
|
2281
|
+
return 'completed';
|
|
2282
|
+
if (type?.includes('failed'))
|
|
2283
|
+
return 'failed';
|
|
2284
|
+
if (type === 'mission_run_started' || type === 'mission_resumed' || type === 'worker_started')
|
|
2285
|
+
return 'running';
|
|
2286
|
+
return null;
|
|
2287
|
+
}
|
|
2288
|
+
function readLatestInteractionStatus(events) {
|
|
2289
|
+
const latestByRequest = new Map();
|
|
2290
|
+
for (const event of events) {
|
|
2291
|
+
const status = readInteractionStatusFromEvent(event.eventType);
|
|
2292
|
+
if (!status)
|
|
2293
|
+
continue;
|
|
2294
|
+
const payload = asRecordValue(event.payload);
|
|
2295
|
+
const requestId = normalizeOptionalString(payload.requestId) ?? `event:${event.eventId}`;
|
|
2296
|
+
const current = latestByRequest.get(requestId);
|
|
2297
|
+
if (!current || event.eventTime >= current.eventTime) {
|
|
2298
|
+
latestByRequest.set(requestId, { eventTime: event.eventTime, status });
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
const latest = Array.from(latestByRequest.values()).sort((left, right) => right.eventTime - left.eventTime)[0];
|
|
2302
|
+
return latest?.status ?? null;
|
|
2303
|
+
}
|
|
2304
|
+
function readInteractionStatusFromEvent(eventType) {
|
|
2305
|
+
if (eventType === 'mission_continue_queued' || eventType === 'mission_interaction_queued')
|
|
2306
|
+
return 'queued';
|
|
2307
|
+
if (eventType === 'mission_interaction_accepted')
|
|
2308
|
+
return 'accepted';
|
|
2309
|
+
if (eventType === 'mission_interaction_delivered')
|
|
2310
|
+
return 'delivered';
|
|
2311
|
+
if (eventType === 'mission_interaction_completed')
|
|
2312
|
+
return 'completed';
|
|
2313
|
+
if (eventType === 'mission_interaction_failed' || eventType === 'mission_interaction_start_failed' || eventType === 'mission_continue_start_failed')
|
|
2314
|
+
return 'failed';
|
|
2315
|
+
return null;
|
|
2316
|
+
}
|
|
2317
|
+
function readArtifactMode(snapshot) {
|
|
2318
|
+
if (Object.keys(snapshot).length === 0)
|
|
2319
|
+
return 'pending';
|
|
2320
|
+
const overview = asRecordValue(snapshot.overview);
|
|
2321
|
+
const artifacts = asRecordValue(snapshot.artifacts);
|
|
2322
|
+
if (normalizeOptionalString(snapshot.droidArtifactImport) === 'runtime_fallback' || normalizeOptionalString(overview.droidArtifactImport) === 'failed') {
|
|
2323
|
+
return 'runtime_fallback';
|
|
2324
|
+
}
|
|
2325
|
+
if (overview.reportOnly === true)
|
|
2326
|
+
return 'report_only';
|
|
2327
|
+
const features = asRecordArray(asRecordValue(snapshot.features).items);
|
|
2328
|
+
const timeline = asRecordArray(asRecordValue(snapshot.timeline).events);
|
|
2329
|
+
const handoffs = asRecordArray(asRecordValue(snapshot.handoffs).items);
|
|
2330
|
+
const validation = asRecordArray(asRecordValue(snapshot.validation).assertions);
|
|
2331
|
+
const reports = asRecordArray(artifacts.reports);
|
|
2332
|
+
if (features.length > 0 || timeline.length > 0 || handoffs.length > 0 || validation.length > 0)
|
|
2333
|
+
return 'structured';
|
|
2334
|
+
if (reports.length > 0)
|
|
2335
|
+
return 'report_only';
|
|
2336
|
+
return 'sparse';
|
|
2337
|
+
}
|
|
2338
|
+
function asRecordValue(value) {
|
|
2339
|
+
return isPlainObject(value) ? value : {};
|
|
2340
|
+
}
|
|
2341
|
+
function asRecordArray(value) {
|
|
2342
|
+
return Array.isArray(value) ? value.filter(isPlainObject) : [];
|
|
2343
|
+
}
|
|
2344
|
+
function assertDroidSnapshotImportable(snapshot, missionDir) {
|
|
2345
|
+
const anomalies = Array.isArray(snapshot.anomalies) ? snapshot.anomalies : [];
|
|
2346
|
+
const blocking = anomalies.filter((item) => {
|
|
2347
|
+
return isPlainObject(item)
|
|
2348
|
+
&& (item.type === 'missing_required_artifact' || item.type === 'invalid_required_artifact');
|
|
2349
|
+
});
|
|
2350
|
+
if (blocking.length > 0) {
|
|
2351
|
+
const overview = isPlainObject(snapshot.overview) ? snapshot.overview : null;
|
|
2352
|
+
const reportOnly = overview?.reportOnly === true && typeof overview.reportCount === 'number' && overview.reportCount > 0;
|
|
2353
|
+
if (!reportOnly) {
|
|
2354
|
+
throw new MissionServiceError(400, `Droid mission artifacts are incomplete or invalid: ${missionDir}`);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
const overview = isPlainObject(snapshot.overview) ? snapshot.overview : null;
|
|
2358
|
+
if (!normalizeOptionalString(overview?.missionId) || !normalizeOptionalString(overview?.state)) {
|
|
2359
|
+
throw new MissionServiceError(400, `Droid mission state is incomplete: ${missionDir}`);
|
|
2360
|
+
}
|
|
2361
|
+
}
|