@polpo-ai/core 0.15.144 → 0.16.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/README.md +30 -0
- package/dist/__tests__/custom-tool-runner.test.d.ts +2 -0
- package/dist/__tests__/custom-tool-runner.test.d.ts.map +1 -0
- package/dist/__tests__/custom-tool-runner.test.js +121 -0
- package/dist/__tests__/custom-tool-runner.test.js.map +1 -0
- package/dist/__tests__/mcp-runner-client.test.d.ts +2 -0
- package/dist/__tests__/mcp-runner-client.test.d.ts.map +1 -0
- package/dist/__tests__/mcp-runner-client.test.js +144 -0
- package/dist/__tests__/mcp-runner-client.test.js.map +1 -0
- package/dist/__tests__/mission-execution.test.d.ts +2 -0
- package/dist/__tests__/mission-execution.test.d.ts.map +1 -0
- package/dist/__tests__/mission-execution.test.js +68 -0
- package/dist/__tests__/mission-execution.test.js.map +1 -0
- package/dist/__tests__/run-state-monitor.test.d.ts +2 -0
- package/dist/__tests__/run-state-monitor.test.d.ts.map +1 -0
- package/dist/__tests__/run-state-monitor.test.js +59 -0
- package/dist/__tests__/run-state-monitor.test.js.map +1 -0
- package/dist/__tests__/runtime-metadata.test.d.ts +2 -0
- package/dist/__tests__/runtime-metadata.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-metadata.test.js +17 -0
- package/dist/__tests__/runtime-metadata.test.js.map +1 -0
- package/dist/__tests__/task-execution-router.test.js +1 -1
- package/dist/__tests__/task-execution-router.test.js.map +1 -1
- package/dist/__tests__/task-runner-agent-lifetime.test.d.ts +2 -0
- package/dist/__tests__/task-runner-agent-lifetime.test.d.ts.map +1 -0
- package/dist/__tests__/task-runner-agent-lifetime.test.js +136 -0
- package/dist/__tests__/task-runner-agent-lifetime.test.js.map +1 -0
- package/dist/adapter.d.ts +18 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/agent-manager.d.ts +3 -0
- package/dist/agent-manager.d.ts.map +1 -1
- package/dist/agent-manager.js +28 -2
- package/dist/agent-manager.js.map +1 -1
- package/dist/agent-store.d.ts +70 -1
- package/dist/agent-store.d.ts.map +1 -1
- package/dist/agent-store.js +103 -1
- package/dist/agent-store.js.map +1 -1
- package/dist/approval-manager.d.ts +24 -4
- package/dist/approval-manager.d.ts.map +1 -1
- package/dist/approval-manager.js +160 -112
- package/dist/approval-manager.js.map +1 -1
- package/dist/approval-manager.test.d.ts +2 -0
- package/dist/approval-manager.test.d.ts.map +1 -0
- package/dist/approval-manager.test.js +116 -0
- package/dist/approval-manager.test.js.map +1 -0
- package/dist/assessment-orchestrator.d.ts +12 -1
- package/dist/assessment-orchestrator.d.ts.map +1 -1
- package/dist/assessment-orchestrator.js +181 -83
- package/dist/assessment-orchestrator.js.map +1 -1
- package/dist/custom-tool-runner.d.ts +56 -0
- package/dist/custom-tool-runner.d.ts.map +1 -0
- package/dist/custom-tool-runner.js +197 -0
- package/dist/custom-tool-runner.js.map +1 -0
- package/dist/escalation-manager.d.ts +2 -0
- package/dist/escalation-manager.d.ts.map +1 -1
- package/dist/escalation-manager.js +79 -25
- package/dist/escalation-manager.js.map +1 -1
- package/dist/events.d.ts +4 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +6 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-capability.d.ts +40 -0
- package/dist/mcp-capability.d.ts.map +1 -0
- package/dist/mcp-capability.js +2 -0
- package/dist/mcp-capability.js.map +1 -0
- package/dist/mcp-runner.d.ts +45 -0
- package/dist/mcp-runner.d.ts.map +1 -0
- package/dist/mcp-runner.js +238 -0
- package/dist/mcp-runner.js.map +1 -0
- package/dist/mission-cancellation.d.ts +14 -0
- package/dist/mission-cancellation.d.ts.map +1 -0
- package/dist/mission-cancellation.js +43 -0
- package/dist/mission-cancellation.js.map +1 -0
- package/dist/mission-execution.d.ts +103 -0
- package/dist/mission-execution.d.ts.map +1 -0
- package/dist/mission-execution.js +196 -0
- package/dist/mission-execution.js.map +1 -0
- package/dist/mission-executor.d.ts +18 -16
- package/dist/mission-executor.d.ts.map +1 -1
- package/dist/mission-executor.js +339 -203
- package/dist/mission-executor.js.map +1 -1
- package/dist/mission-gating.d.ts +11 -15
- package/dist/mission-gating.d.ts.map +1 -1
- package/dist/mission-gating.js +25 -33
- package/dist/mission-gating.js.map +1 -1
- package/dist/mission-inspection.d.ts +29 -0
- package/dist/mission-inspection.d.ts.map +1 -0
- package/dist/mission-inspection.js +99 -0
- package/dist/mission-inspection.js.map +1 -0
- package/dist/mission-store.d.ts +7 -0
- package/dist/mission-store.d.ts.map +1 -1
- package/dist/mission-store.js +8 -0
- package/dist/mission-store.js.map +1 -1
- package/dist/orchestrator-context.d.ts +3 -0
- package/dist/orchestrator-context.d.ts.map +1 -1
- package/dist/orchestrator-engine.d.ts +39 -39
- package/dist/orchestrator-engine.d.ts.map +1 -1
- package/dist/orchestrator-engine.js +124 -24
- package/dist/orchestrator-engine.js.map +1 -1
- package/dist/run-state-monitor.d.ts +14 -0
- package/dist/run-state-monitor.d.ts.map +1 -0
- package/dist/run-state-monitor.js +63 -0
- package/dist/run-state-monitor.js.map +1 -0
- package/dist/run-store.d.ts +15 -0
- package/dist/run-store.d.ts.map +1 -1
- package/dist/runner-capability.d.ts +43 -0
- package/dist/runner-capability.d.ts.map +1 -0
- package/dist/runner-capability.js +55 -0
- package/dist/runner-capability.js.map +1 -0
- package/dist/runtime-metadata.d.ts +5 -0
- package/dist/runtime-metadata.d.ts.map +1 -0
- package/dist/runtime-metadata.js +21 -0
- package/dist/runtime-metadata.js.map +1 -0
- package/dist/scheduler.d.ts +2 -1
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +16 -6
- package/dist/scheduler.js.map +1 -1
- package/dist/schemas.d.ts +8 -6
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +10 -0
- package/dist/schemas.js.map +1 -1
- package/dist/spawner.d.ts +12 -0
- package/dist/spawner.d.ts.map +1 -1
- package/dist/steering.d.ts +4 -1
- package/dist/steering.d.ts.map +1 -1
- package/dist/steering.js.map +1 -1
- package/dist/task-approval-coordinator.d.ts +29 -0
- package/dist/task-approval-coordinator.d.ts.map +1 -0
- package/dist/task-approval-coordinator.js +216 -0
- package/dist/task-approval-coordinator.js.map +1 -0
- package/dist/task-dispatch.d.ts +40 -0
- package/dist/task-dispatch.d.ts.map +1 -0
- package/dist/task-dispatch.js +136 -0
- package/dist/task-dispatch.js.map +1 -0
- package/dist/task-execution.d.ts +146 -0
- package/dist/task-execution.d.ts.map +1 -0
- package/dist/task-execution.js +357 -0
- package/dist/task-execution.js.map +1 -0
- package/dist/task-manager.d.ts +31 -22
- package/dist/task-manager.d.ts.map +1 -1
- package/dist/task-manager.js +74 -7
- package/dist/task-manager.js.map +1 -1
- package/dist/task-runner.d.ts +30 -3
- package/dist/task-runner.d.ts.map +1 -1
- package/dist/task-runner.js +429 -90
- package/dist/task-runner.js.map +1 -1
- package/dist/task-store.d.ts +9 -3
- package/dist/task-store.d.ts.map +1 -1
- package/dist/tool-invocation.d.ts +4 -0
- package/dist/tool-invocation.d.ts.map +1 -1
- package/dist/tool-invocation.js +8 -1
- package/dist/tool-invocation.js.map +1 -1
- package/dist/tool-invocation.test.d.ts +2 -0
- package/dist/tool-invocation.test.d.ts.map +1 -0
- package/dist/tool-invocation.test.js +40 -0
- package/dist/tool-invocation.test.js.map +1 -0
- package/dist/types/agent.d.ts +4 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/agent.js.map +1 -1
- package/dist/types/config.d.ts +3 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/mission.d.ts +23 -1
- package/dist/types/mission.d.ts.map +1 -1
- package/dist/types/task.d.ts +10 -0
- package/dist/types/task.d.ts.map +1 -1
- package/package.json +17 -1
package/dist/mission-executor.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { currentMissionExecutionTasks, resumableMissions } from "./mission-inspection.js";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import { parseMissionDocument } from "./schemas.js";
|
|
4
|
+
import { resolveMissionStore, resolveMissionForGroup, resolveMissionExecutionStore } from "./mission-store.js";
|
|
5
|
+
import { assertMissionExecutionTask, MissionExecutionError, validateMissionExecutionKey } from "./mission-execution.js";
|
|
6
|
+
import { cancelMissionExecution } from "./mission-cancellation.js";
|
|
3
7
|
import { InMemoryCheckpointStore, InMemoryDelayStore } from "./in-memory-stores.js";
|
|
4
8
|
import { MissionGating, parseISO8601Duration } from "./mission-gating.js";
|
|
5
9
|
import { resolveConfiguredModelSelection } from "./model-profiles.js";
|
|
10
|
+
import { normalizeRuntimeMetadata } from "./runtime-metadata.js";
|
|
6
11
|
/**
|
|
7
12
|
* Mission CRUD + execution + resume + group lifecycle.
|
|
8
13
|
* Runtime checkpoint/delay gating is delegated to MissionGating.
|
|
@@ -22,6 +27,18 @@ export class MissionExecutor {
|
|
|
22
27
|
/** Track the pre-execution status for scheduled/recurring missions (by group name).
|
|
23
28
|
* When a mission completes/fails, this determines whether to return to scheduled/recurring. */
|
|
24
29
|
scheduledOrigin = new Map();
|
|
30
|
+
preparedExecutions = new Map();
|
|
31
|
+
groupExecutions = new Map();
|
|
32
|
+
gatingKey(group) {
|
|
33
|
+
if (this.preparedExecutions.has(group))
|
|
34
|
+
return group;
|
|
35
|
+
const executions = this.groupExecutions.get(group);
|
|
36
|
+
if (!executions?.size)
|
|
37
|
+
return group;
|
|
38
|
+
if (executions.size > 1)
|
|
39
|
+
throw new MissionExecutionError("mission_execution_conflict", "Ambiguous Mission group; use its execution ID");
|
|
40
|
+
return [...executions][0];
|
|
41
|
+
}
|
|
25
42
|
constructor(ctx, taskMgr, agentMgr) {
|
|
26
43
|
this.ctx = ctx;
|
|
27
44
|
this.taskMgr = taskMgr;
|
|
@@ -45,13 +62,34 @@ export class MissionExecutor {
|
|
|
45
62
|
const allTasks = await this.ctx.taskStore.listTasks();
|
|
46
63
|
const groups = new Set();
|
|
47
64
|
for (const t of allTasks) {
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
const group = t.missionExecutionId ?? t.group;
|
|
66
|
+
if (group)
|
|
67
|
+
groups.add(group);
|
|
50
68
|
}
|
|
51
69
|
for (const group of groups) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
70
|
+
try {
|
|
71
|
+
const groupTasks = allTasks.filter(t => (t.missionExecutionId ?? t.group) === group);
|
|
72
|
+
const persisted = groupTasks[0];
|
|
73
|
+
// Presentation must retain its durable key even if receipt recovery is
|
|
74
|
+
// temporarily unavailable. Authorization still requires the receipt below.
|
|
75
|
+
if (persisted.missionExecutionId)
|
|
76
|
+
this.gating.setExecutionIdentity({
|
|
77
|
+
executionId: persisted.missionExecutionId, missionId: persisted.missionId, group: persisted.group ?? group,
|
|
78
|
+
});
|
|
79
|
+
if (groupTasks.every(t => t.status === "done" || t.status === "failed")) {
|
|
80
|
+
const mission = await resolveMissionForGroup(this.missions, groupTasks, group);
|
|
81
|
+
const receipt = groupTasks[0].missionExecutionId
|
|
82
|
+
? (await resolveMissionExecutionStore(this.ctx)?.getMissionExecutionById(groupTasks[0].missionExecutionId))?.receipt : undefined;
|
|
83
|
+
if (mission?.status !== "active" || (receipt && receipt.sequence !== mission.executionCount))
|
|
84
|
+
this.cleanedGroups.add(group);
|
|
85
|
+
}
|
|
86
|
+
if (!this.cleanedGroups.has(group) && groupTasks[0].missionExecutionId)
|
|
87
|
+
await this.prepareTaskExecution(groupTasks[0], false);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// A receipt outage blocks only this execution. The dispatch barrier and
|
|
91
|
+
// cleanup retry recovery later; unrelated Missions remain available.
|
|
92
|
+
this.ctx.emitter.emit("log", { level: "warn", message: `Mission execution recovery deferred for ${group}` });
|
|
55
93
|
}
|
|
56
94
|
}
|
|
57
95
|
}
|
|
@@ -61,26 +99,38 @@ export class MissionExecutor {
|
|
|
61
99
|
}
|
|
62
100
|
/** Get quality gates for a mission group. Returns empty array if none defined. */
|
|
63
101
|
getQualityGates(group) {
|
|
64
|
-
return this.gatesByGroup.get(group) ?? [];
|
|
102
|
+
return this.gatesByGroup.get(this.gatingKey(group)) ?? [];
|
|
65
103
|
}
|
|
66
104
|
// ─── Checkpoint/delay gating (delegated to MissionGating) ─────────────
|
|
67
105
|
/** Get checkpoints for a mission group. Returns empty array if none defined. */
|
|
68
106
|
getCheckpoints(group) {
|
|
69
|
-
return this.gating.getCheckpoints(group);
|
|
107
|
+
return this.gating.getCheckpoints(this.gatingKey(group));
|
|
70
108
|
}
|
|
71
109
|
/**
|
|
72
110
|
* Check if a task is blocked by an active (unresumed) checkpoint.
|
|
73
111
|
* Returns the blocking checkpoint if found, undefined if the task can proceed.
|
|
74
112
|
*/
|
|
75
113
|
async getBlockingCheckpoint(group, taskTitle, taskId, tasks) {
|
|
76
|
-
|
|
114
|
+
const key = this.gatingKey(group);
|
|
115
|
+
return this.gating.getBlockingCheckpoint(key, taskTitle, taskId, this.preparedExecutions.has(key) ? tasks.filter(task => task.missionExecutionId === key) : tasks);
|
|
77
116
|
}
|
|
78
117
|
/**
|
|
79
118
|
* Resume a checkpoint, unblocking its blocksTasks.
|
|
80
119
|
* Returns true if the checkpoint was active and is now resumed, false if not found.
|
|
81
120
|
*/
|
|
82
121
|
async resumeCheckpoint(group, checkpointName) {
|
|
83
|
-
return this.gating.resumeCheckpoint(group, checkpointName);
|
|
122
|
+
return this.gating.resumeCheckpoint(this.gatingKey(group), checkpointName);
|
|
123
|
+
}
|
|
124
|
+
async resumeCheckpointByMissionId(missionId, checkpointName) {
|
|
125
|
+
await this.ready;
|
|
126
|
+
const mission = await this.getMission(missionId);
|
|
127
|
+
if (!mission)
|
|
128
|
+
return false;
|
|
129
|
+
const task = (await this.currentExecutionTasks(mission, await this.ctx.taskStore.listTasks()))[0];
|
|
130
|
+
if (!task)
|
|
131
|
+
return false;
|
|
132
|
+
await this.prepareTaskExecution(task, false);
|
|
133
|
+
return this.resumeCheckpoint(task.missionExecutionId ?? task.group ?? mission.name, checkpointName);
|
|
84
134
|
}
|
|
85
135
|
/** Get all active (unresumed) checkpoints across all mission groups. */
|
|
86
136
|
getActiveCheckpoints() {
|
|
@@ -88,7 +138,7 @@ export class MissionExecutor {
|
|
|
88
138
|
}
|
|
89
139
|
/** Get delays for a mission group. Returns empty array if none defined. */
|
|
90
140
|
getDelays(group) {
|
|
91
|
-
return this.gating.getDelays(group);
|
|
141
|
+
return this.gating.getDelays(this.gatingKey(group));
|
|
92
142
|
}
|
|
93
143
|
/**
|
|
94
144
|
* Check if a task is blocked by an active (unexpired) delay.
|
|
@@ -97,13 +147,15 @@ export class MissionExecutor {
|
|
|
97
147
|
* Returns the blocking delay if found, undefined if the task can proceed.
|
|
98
148
|
*/
|
|
99
149
|
async getBlockingDelay(group, taskTitle, taskId, tasks) {
|
|
100
|
-
|
|
150
|
+
const key = this.gatingKey(group);
|
|
151
|
+
return this.gating.getBlockingDelay(key, taskTitle, taskId, this.preparedExecutions.has(key) ? tasks.filter(task => task.missionExecutionId === key) : tasks);
|
|
101
152
|
}
|
|
102
153
|
/** Get all active (unexpired) delays across all mission groups. */
|
|
103
154
|
getActiveDelays() {
|
|
104
155
|
return this.gating.getActiveDelays();
|
|
105
156
|
}
|
|
106
157
|
async createMission(opts) {
|
|
158
|
+
const metadata = normalizeRuntimeMetadata(opts.metadata);
|
|
107
159
|
const name = opts.name ?? await this.missions.nextMissionName();
|
|
108
160
|
const mission = await this.missions.createMission({
|
|
109
161
|
name,
|
|
@@ -115,6 +167,7 @@ export class MissionExecutor {
|
|
|
115
167
|
endDate: opts.endDate,
|
|
116
168
|
notifications: opts.notifications,
|
|
117
169
|
user: opts.user,
|
|
170
|
+
metadata,
|
|
118
171
|
});
|
|
119
172
|
this.ctx.emitter.emit("mission:saved", { missionId: mission.id, name: mission.name, status: mission.status });
|
|
120
173
|
return mission;
|
|
@@ -135,32 +188,20 @@ export class MissionExecutor {
|
|
|
135
188
|
const mission = await this.getMission(missionId);
|
|
136
189
|
if (!mission)
|
|
137
190
|
return false;
|
|
138
|
-
|
|
139
|
-
//
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// Also clean numbered groups for recurring missions (e.g. "mission-1 #2", "mission-1 #3")
|
|
147
|
-
for (const key of this.gatesByGroup.keys()) {
|
|
148
|
-
if (key.startsWith(missionGroup + " #"))
|
|
149
|
-
this.gatesByGroup.delete(key);
|
|
150
|
-
}
|
|
151
|
-
// 4. Clean up persisted checkpoints and delays (incl. numbered recurring-run groups)
|
|
152
|
-
await this.gating.removeGroupAndRuns(missionGroup);
|
|
153
|
-
// 5. Clean up scheduled origin cache
|
|
154
|
-
this.scheduledOrigin.delete(missionGroup);
|
|
155
|
-
for (const key of this.scheduledOrigin.keys()) {
|
|
156
|
-
if (key.startsWith(missionGroup + " #"))
|
|
157
|
-
this.scheduledOrigin.delete(key);
|
|
191
|
+
await this.ready;
|
|
192
|
+
// An explicit Mission ID always wins over the legacy display-label fallback.
|
|
193
|
+
const belongs = (task) => task.missionId === missionId || (!task.missionId && task.group === mission.name);
|
|
194
|
+
const tasks = (await this.ctx.taskStore.listTasks()).filter(belongs);
|
|
195
|
+
const groups = new Set(tasks.map(task => task.missionExecutionId ?? task.group).filter((group) => !!group));
|
|
196
|
+
for (const [key, pending] of this.preparedExecutions) {
|
|
197
|
+
if ((await pending).receipt.missionId === missionId)
|
|
198
|
+
groups.add(key);
|
|
158
199
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
200
|
+
const deletedTasks = await this.taskMgr.clearTasks(belongs);
|
|
201
|
+
for (const group of groups) {
|
|
202
|
+
await this.agentMgr.cleanupVolatileAgents(group);
|
|
203
|
+
await this.forgetExecution(group);
|
|
204
|
+
this.cleanedGroups.delete(group);
|
|
164
205
|
}
|
|
165
206
|
// ── Delete the mission record ────────────────────────
|
|
166
207
|
const result = await this.missions.deleteMission(missionId);
|
|
@@ -169,193 +210,282 @@ export class MissionExecutor {
|
|
|
169
210
|
}
|
|
170
211
|
return result;
|
|
171
212
|
}
|
|
213
|
+
async currentExecutionTasks(mission, tasks) {
|
|
214
|
+
return currentMissionExecutionTasks(mission, tasks, resolveMissionExecutionStore(this.ctx));
|
|
215
|
+
}
|
|
172
216
|
async getResumableMissions() {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return missions.filter(m => {
|
|
176
|
-
// Non-resumable statuses: draft (never executed), scheduled/recurring (scheduler handles),
|
|
177
|
-
// completed (done), cancelled (aborted)
|
|
178
|
-
if (m.status === "draft" || m.status === "scheduled" || m.status === "recurring" ||
|
|
179
|
-
m.status === "completed" || m.status === "cancelled")
|
|
180
|
-
return false;
|
|
181
|
-
const tasks = state.tasks.filter(t => t.group === m.name);
|
|
182
|
-
if (tasks.length === 0)
|
|
183
|
-
return false;
|
|
184
|
-
return tasks.some(t => t.status === "pending" || t.status === "failed");
|
|
185
|
-
});
|
|
217
|
+
await this.ready;
|
|
218
|
+
return resumableMissions(await this.listMissions(), await this.ctx.taskStore.listTasks(), resolveMissionExecutionStore(this.ctx));
|
|
186
219
|
}
|
|
187
220
|
async resumeMission(missionId, opts) {
|
|
221
|
+
await this.ready;
|
|
188
222
|
const mission = await this.getMission(missionId);
|
|
189
223
|
if (!mission)
|
|
190
224
|
throw new Error("Mission not found");
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
continue;
|
|
201
|
-
const { name, ...rest } = a;
|
|
202
|
-
await this.agentMgr.addVolatileAgent({ name, ...rest }, mission.name);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
catch (err) {
|
|
207
|
-
this.ctx.emitter.emit("log", { level: "warn", message: `Failed to re-register volatile agents for ${mission.name}: ${err instanceof Error ? err.message : String(err)}` });
|
|
208
|
-
}
|
|
225
|
+
const tasks = await this.currentExecutionTasks(mission, await this.ctx.taskStore.listTasks());
|
|
226
|
+
for (const task of tasks)
|
|
227
|
+
this.cleanedGroups.delete(task.missionExecutionId ?? task.group ?? mission.name);
|
|
228
|
+
// Old CRUD-only executions have no accepted snapshot. Preserve their legacy
|
|
229
|
+
// resume behavior without letting edited configuration replace durable plans.
|
|
230
|
+
if (tasks.some(task => !task.missionExecutionId) && this.ctx.config.settings.enableVolatileTeams !== false) {
|
|
231
|
+
const doc = parseMissionDocument(JSON.parse(mission.data));
|
|
232
|
+
for (const agent of doc.team ?? [])
|
|
233
|
+
await this.agentMgr.addVolatileAgent(agent, tasks[0]?.group ?? mission.name);
|
|
209
234
|
}
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
const failedTasks = tasks.filter(t => t.status === "failed");
|
|
213
|
-
const pendingTasks = tasks.filter(t => t.status === "pending");
|
|
235
|
+
const failedTasks = tasks.filter(task => task.status === "failed");
|
|
236
|
+
const pendingTasks = tasks.filter(task => task.status === "pending");
|
|
214
237
|
let retried = 0;
|
|
215
238
|
if (opts?.retryFailed) {
|
|
216
239
|
for (const task of failedTasks) {
|
|
240
|
+
// Validate the receipt before making failed work runnable again.
|
|
241
|
+
await this.prepareTaskExecution(task);
|
|
217
242
|
try {
|
|
218
243
|
await this.taskMgr.retryTask(task.id);
|
|
219
244
|
retried++;
|
|
220
245
|
}
|
|
221
|
-
catch {
|
|
246
|
+
catch {
|
|
247
|
+
continue; /* no retries left */
|
|
222
248
|
}
|
|
249
|
+
await this.prepareTaskExecution((await this.ctx.taskStore.getTask(task.id)));
|
|
223
250
|
}
|
|
224
251
|
}
|
|
225
|
-
|
|
252
|
+
for (const task of pendingTasks)
|
|
253
|
+
await this.prepareTaskExecution(task);
|
|
254
|
+
if (mission.status === "failed")
|
|
226
255
|
await this.updateMission(missionId, { status: "active" });
|
|
227
|
-
}
|
|
228
256
|
this.ctx.emitter.emit("mission:resumed", { missionId, name: mission.name, retried, pending: pendingTasks.length });
|
|
229
257
|
return { retried, pending: pendingTasks.length };
|
|
230
258
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// Increment execution count (tracks how many times this mission has run — useful for recurring)
|
|
242
|
-
const runNumber = (mission.executionCount ?? 0) + 1;
|
|
243
|
-
await this.missions.updateMission(missionId, { executionCount: runNumber });
|
|
244
|
-
// Validate mission document through Zod schema — throws with clear error on invalid shape
|
|
245
|
-
const raw = JSON.parse(mission.data);
|
|
246
|
-
const doc = parseMissionDocument(raw);
|
|
247
|
-
// For recurring/scheduled missions with multiple runs, disambiguate the group with a run number
|
|
248
|
-
const group = runNumber > 1 ? `${mission.name} #${runNumber}` : mission.name;
|
|
249
|
-
// Run before:mission:execute hook
|
|
250
|
-
const hookResult = this.ctx.hooks.runBeforeSync("mission:execute", {
|
|
251
|
-
missionId,
|
|
252
|
-
mission,
|
|
253
|
-
taskCount: doc.tasks.length,
|
|
254
|
-
});
|
|
255
|
-
if (hookResult.cancelled) {
|
|
256
|
-
throw new Error(`Mission execution blocked by hook: ${hookResult.cancelReason ?? "no reason"}`);
|
|
257
|
-
}
|
|
258
|
-
// Register volatile agents from the mission's team section
|
|
259
|
-
const enableVolatile = this.ctx.config.settings.enableVolatileTeams !== false;
|
|
260
|
-
if (enableVolatile && doc.team && Array.isArray(doc.team)) {
|
|
261
|
-
for (const a of doc.team) {
|
|
262
|
-
if (!a.name)
|
|
263
|
-
continue;
|
|
264
|
-
const { name, ...rest } = a;
|
|
265
|
-
await this.agentMgr.addVolatileAgent({ name, ...rest }, group);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
// Validate API keys for all agents referenced in the mission
|
|
269
|
-
const allAgents = await this.agentMgr.getAgents();
|
|
270
|
-
const referencedModels = [];
|
|
271
|
-
for (const t of doc.tasks) {
|
|
272
|
-
const agentName = t.assignTo || allAgents[0]?.name;
|
|
273
|
-
const agent = allAgents.find(a => a.name === agentName);
|
|
274
|
-
if (agent?.model) {
|
|
275
|
-
referencedModels.push(...resolveConfiguredModelSelection(agent.model, this.ctx.config.settings, agent.allowedModelProfiles).policy.candidates);
|
|
259
|
+
/** Cancel one accepted execution, never a presentation group or a newer Task attempt. */
|
|
260
|
+
async abortMission(missionId, options = {}) {
|
|
261
|
+
const capturedOptions = { ...options };
|
|
262
|
+
await this.ready;
|
|
263
|
+
const result = await cancelMissionExecution({ missionStore: this.missions, taskStore: this.ctx.taskStore,
|
|
264
|
+
missionExecutionStore: resolveMissionExecutionStore(this.ctx) }, missionId, capturedOptions);
|
|
265
|
+
// Durable state wins even if an in-process owner or cache is unavailable.
|
|
266
|
+
for (const runId of result.killedRunIds) {
|
|
267
|
+
try {
|
|
268
|
+
await this.ctx.spawner?.cancelRun?.(runId);
|
|
276
269
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
`Available agents: ${allAgents.map(a => a.name).join(", ")}`);
|
|
270
|
+
catch {
|
|
271
|
+
this.ctx.emitter.emit("log", { level: "warn", message: `Mission cancellation committed; owner notification deferred for Run ${runId}` });
|
|
280
272
|
}
|
|
281
273
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
274
|
+
// Do not remove execution resources here: a retry may already be using the
|
|
275
|
+
// same receipt while owner notification is pending. Normal lifecycle cleanup
|
|
276
|
+
// owns those resources independently from this captured cancellation.
|
|
277
|
+
return { aborted: result.aborted, executionId: result.executionId, missionCancelled: result.missionCancelled };
|
|
278
|
+
}
|
|
279
|
+
async forgetExecution(group) {
|
|
280
|
+
// Settle recovery before removing its definitions, so an in-flight load
|
|
281
|
+
// cannot restore a cache that cleanup has just removed.
|
|
282
|
+
await this.preparedExecutions.get(group);
|
|
283
|
+
this.gatesByGroup.delete(group);
|
|
284
|
+
this.scheduledOrigin.delete(group);
|
|
285
|
+
await this.gating.removeGroup(group);
|
|
286
|
+
this.preparedExecutions.delete(group);
|
|
287
|
+
for (const [label, aliases] of this.groupExecutions) {
|
|
288
|
+
aliases.delete(group);
|
|
289
|
+
if (!aliases.size)
|
|
290
|
+
this.groupExecutions.delete(label);
|
|
291
291
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
292
|
+
}
|
|
293
|
+
/** Restore accepted runtime definitions before any task can be dispatched.
|
|
294
|
+
* The durable receipt is authoritative even after Mission edits or a restart. */
|
|
295
|
+
async prepareTaskExecution(task, restoreAgents = true) {
|
|
296
|
+
if (!task.missionExecutionId)
|
|
297
|
+
return;
|
|
298
|
+
const store = resolveMissionExecutionStore(this.ctx);
|
|
299
|
+
if (!store)
|
|
300
|
+
throw new MissionExecutionError("mission_execution_unsupported", "Mission execution recovery is unavailable");
|
|
301
|
+
let pending = this.preparedExecutions.get(task.missionExecutionId);
|
|
302
|
+
if (!pending) {
|
|
303
|
+
pending = (async () => {
|
|
304
|
+
await this.gating.ready;
|
|
305
|
+
const admission = await store.getMissionExecutionById(task.missionExecutionId);
|
|
306
|
+
if (!admission)
|
|
307
|
+
throw new MissionExecutionError("mission_execution_invalid", "Mission execution receipt is missing");
|
|
308
|
+
const doc = parseMissionDocument(JSON.parse(admission.expected.data));
|
|
309
|
+
const titles = new Map(admission.runtime?.taskTitles?.map(entry => [entry.authored, entry.accepted]));
|
|
310
|
+
const translate = (items = []) => items.map(item => ({
|
|
311
|
+
...item, afterTasks: item.afterTasks.map(title => titles.get(title) ?? title), blocksTasks: item.blocksTasks.map(title => titles.get(title) ?? title),
|
|
312
|
+
}));
|
|
313
|
+
const group = admission.receipt.executionId;
|
|
314
|
+
this.gating.setExecutionIdentity({ executionId: group, missionId: admission.receipt.missionId, group: admission.receipt.group });
|
|
315
|
+
const aliases = this.groupExecutions.get(admission.receipt.group) ?? new Set();
|
|
316
|
+
aliases.add(group);
|
|
317
|
+
this.groupExecutions.set(admission.receipt.group, aliases);
|
|
318
|
+
// Definitions already persist in the receipt. Do not rewrite shared
|
|
319
|
+
// checkpoint state from a second executor during admission recovery.
|
|
320
|
+
this.gatesByGroup.set(group, translate(doc.qualityGates));
|
|
321
|
+
await this.gating.setCheckpointDefinitions(group, translate(doc.checkpoints), false);
|
|
322
|
+
await this.gating.setDelayDefinitions(group, translate(doc.delays), false);
|
|
323
|
+
if (admission.expected.status === "scheduled" || admission.expected.status === "recurring") {
|
|
324
|
+
this.scheduledOrigin.set(group, admission.expected.status);
|
|
306
325
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
assignTo: t.assignTo || (await this.agentMgr.getAgents())[0]?.name || "default",
|
|
312
|
-
dependsOn: deps,
|
|
313
|
-
expectations,
|
|
314
|
-
expectedOutcomes: t.expectedOutcomes,
|
|
315
|
-
group,
|
|
316
|
-
missionId,
|
|
317
|
-
maxDuration: t.maxDuration,
|
|
318
|
-
retryPolicy: t.retryPolicy,
|
|
319
|
-
notifications: t.notifications,
|
|
320
|
-
sideEffects: t.sideEffects,
|
|
321
|
-
});
|
|
322
|
-
titleToId.set(t.title, task.id);
|
|
323
|
-
tasks.push(task);
|
|
326
|
+
return admission;
|
|
327
|
+
})();
|
|
328
|
+
this.preparedExecutions.set(task.missionExecutionId, pending);
|
|
329
|
+
pending.catch(() => this.preparedExecutions.delete(task.missionExecutionId));
|
|
324
330
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
+
const admission = await pending;
|
|
332
|
+
assertMissionExecutionTask(task, admission.receipt);
|
|
333
|
+
// Never recreate volatile agents for historical/terminal work. New active
|
|
334
|
+
// tasks recover them from the accepted plan, not the edited Mission data.
|
|
335
|
+
if (restoreAgents && task.status !== "done" && task.status !== "failed") {
|
|
336
|
+
for (const agent of admission.runtime?.volatileAgents ?? []) {
|
|
337
|
+
await this.agentMgr.addVolatileAgent(agent, admission.receipt.executionId);
|
|
338
|
+
}
|
|
331
339
|
}
|
|
332
|
-
|
|
333
|
-
|
|
340
|
+
}
|
|
341
|
+
async executeMission(missionId, options = {}, expectedMission) {
|
|
342
|
+
// Trusted scheduler input, never exposed as an authored HTTP option. A
|
|
343
|
+
// delayed trigger must not obtain fresh authority by re-reading the Mission.
|
|
344
|
+
const expected = expectedMission === undefined ? undefined : structuredClone(expectedMission);
|
|
345
|
+
options = { ...options };
|
|
346
|
+
await this.ready;
|
|
347
|
+
const persistence = resolveMissionExecutionStore(this.ctx);
|
|
348
|
+
if (expected && (!persistence || expected.id !== missionId)) {
|
|
349
|
+
throw new MissionExecutionError("mission_execution_unsupported", "Guarded Mission execution requires matching atomic persistence");
|
|
334
350
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
351
|
+
if (options.idempotencyKey !== undefined) {
|
|
352
|
+
validateMissionExecutionKey(missionId, options.idempotencyKey);
|
|
353
|
+
if (!persistence)
|
|
354
|
+
throw new MissionExecutionError("mission_execution_unsupported", "Idempotent Mission execution requires atomic persistence");
|
|
355
|
+
const accepted = await persistence.getMissionExecution(missionId, options.idempotencyKey);
|
|
356
|
+
if (accepted)
|
|
357
|
+
return { tasks: accepted.tasks, group: accepted.group, executionId: accepted.executionId };
|
|
338
358
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
await this.missions.
|
|
359
|
+
let admitted = false;
|
|
360
|
+
try {
|
|
361
|
+
const stored = expected ?? await this.missions.getMission(missionId);
|
|
362
|
+
if (!stored)
|
|
363
|
+
throw new Error("Mission not found");
|
|
364
|
+
// Some custom stores return mutable objects. Hooks and awaits must not alter
|
|
365
|
+
// the expected snapshot that will be compared during atomic admission.
|
|
366
|
+
const mission = structuredClone(stored);
|
|
367
|
+
if (!["draft", "scheduled", "recurring", "failed", "cancelled"].includes(mission.status)) {
|
|
368
|
+
throw new Error(`Cannot execute mission in "${mission.status}" state`);
|
|
369
|
+
}
|
|
370
|
+
const sequence = (mission.executionCount ?? 0) + 1;
|
|
371
|
+
const group = sequence > 1 ? `${mission.name} #${sequence}` : mission.name;
|
|
372
|
+
const doc = parseMissionDocument(JSON.parse(mission.data));
|
|
373
|
+
const hook = await this.ctx.hooks.runBefore("mission:execute", { missionId, mission: structuredClone(mission), taskCount: doc.tasks.length });
|
|
374
|
+
if (hook.cancelled)
|
|
375
|
+
throw new Error(`Mission execution blocked by hook: ${hook.cancelReason ?? "no reason"}`);
|
|
376
|
+
const existingAgents = await this.agentMgr.getAgents();
|
|
377
|
+
const volatileAgents = this.ctx.config.settings.enableVolatileTeams === false ? [] : (doc.team ?? []).filter(agent => {
|
|
378
|
+
if (!agent || typeof agent.name !== "string" || !agent.name.trim())
|
|
379
|
+
throw new Error("Mission team requires valid agent names");
|
|
380
|
+
return !existingAgents.some(existing => existing.name === agent.name);
|
|
381
|
+
});
|
|
382
|
+
const agents = [...existingAgents, ...volatileAgents];
|
|
383
|
+
// Resolve all dependencies, including forward references, before writing.
|
|
384
|
+
const byTitle = new Map(doc.tasks.map(task => [task.title, task]));
|
|
385
|
+
const ordered = [], visited = new Set(), visiting = new Set();
|
|
386
|
+
const visit = (title) => {
|
|
387
|
+
if (visited.has(title))
|
|
388
|
+
return;
|
|
389
|
+
if (visiting.has(title))
|
|
390
|
+
throw new Error(`Cyclic Mission dependency at "${title}"`);
|
|
391
|
+
const task = byTitle.get(title);
|
|
392
|
+
if (!task)
|
|
393
|
+
throw new Error(`Unknown Mission dependency "${title}"`);
|
|
394
|
+
visiting.add(title);
|
|
395
|
+
for (const dependency of task.dependsOn ?? [])
|
|
396
|
+
visit(dependency);
|
|
397
|
+
visiting.delete(title);
|
|
398
|
+
visited.add(title);
|
|
399
|
+
ordered.push(task);
|
|
400
|
+
};
|
|
401
|
+
for (const task of doc.tasks)
|
|
402
|
+
visit(task.title);
|
|
403
|
+
const createdAt = new Date().toISOString(), executionId = nanoid();
|
|
404
|
+
const titleToId = new Map(), tasks = [];
|
|
405
|
+
const referencedModels = [];
|
|
406
|
+
const taskTitles = [];
|
|
407
|
+
for (const authored of ordered) {
|
|
408
|
+
const prepared = await this.taskMgr.prepareTask({
|
|
409
|
+
title: authored.title, description: authored.description || authored.title,
|
|
410
|
+
assignTo: authored.assignTo || agents[0]?.name || "default",
|
|
411
|
+
dependsOn: (authored.dependsOn ?? []).map(title => titleToId.get(title)),
|
|
412
|
+
expectations: authored.expectations, expectedOutcomes: authored.expectedOutcomes,
|
|
413
|
+
group, missionId, user: mission.user, metadata: mission.metadata, maxDuration: authored.maxDuration,
|
|
414
|
+
retryPolicy: authored.retryPolicy, notifications: authored.notifications, sideEffects: authored.sideEffects,
|
|
415
|
+
});
|
|
416
|
+
const agent = agents.find(candidate => candidate.name === prepared.assignTo);
|
|
417
|
+
if (!agent)
|
|
418
|
+
throw new Error(`Mission references agent "${prepared.assignTo}" (task "${prepared.title}") but no such agent exists. Available agents: ${agents.map(agent => agent.name).join(", ")}`);
|
|
419
|
+
if (agent.model)
|
|
420
|
+
referencedModels.push(...resolveConfiguredModelSelection(agent.model, this.ctx.config.settings, agent.allowedModelProfiles).policy.candidates);
|
|
421
|
+
const task = { ...prepared, id: nanoid(), status: "pending", retries: 0, createdAt, updatedAt: createdAt,
|
|
422
|
+
missionId, group, user: mission.user, metadata: normalizeRuntimeMetadata(mission.metadata), ...(persistence ? { missionExecutionId: executionId } : {}) };
|
|
423
|
+
tasks.push(task);
|
|
424
|
+
titleToId.set(authored.title, task.id);
|
|
425
|
+
taskTitles.push({ authored: authored.title, accepted: task.title });
|
|
426
|
+
}
|
|
427
|
+
if (referencedModels.length && this.ctx.validateProviderKeys) {
|
|
428
|
+
const missing = this.ctx.validateProviderKeys(referencedModels);
|
|
429
|
+
if (missing.length)
|
|
430
|
+
throw new Error(`Missing API keys for providers: ${missing.map(model => `${model.provider} (model: ${model.modelSpec})`).join(", ")}. Set the corresponding environment variables or add them to the project.json providers section.`);
|
|
431
|
+
}
|
|
432
|
+
if (persistence) {
|
|
433
|
+
const result = await persistence.commitMissionExecution({ expected: mission, notifications: doc.notifications,
|
|
434
|
+
runtime: { volatileAgents, taskTitles }, receipt: { executionId, missionId, idempotencyKey: options.idempotencyKey ?? `execution:${executionId}`,
|
|
435
|
+
group, sequence, user: mission.user, metadata: normalizeRuntimeMetadata(mission.metadata), createdAt, tasks } });
|
|
436
|
+
admitted = true;
|
|
437
|
+
if (result.created) {
|
|
438
|
+
await this.prepareTaskExecution(result.receipt.tasks[0]);
|
|
439
|
+
for (const task of result.receipt.tasks)
|
|
440
|
+
this.taskMgr.notifyTaskCreated(task);
|
|
441
|
+
this.ctx.emitter.emit("mission:executed", { missionId, group, taskCount: result.receipt.tasks.length });
|
|
442
|
+
}
|
|
443
|
+
return { tasks: result.receipt.tasks, group: result.receipt.group, executionId: result.receipt.executionId };
|
|
444
|
+
}
|
|
445
|
+
// Compatibility for custom stores with only ordinary CRUD. Explicit
|
|
446
|
+
// idempotency is refused above; these writes do not claim atomic admission.
|
|
447
|
+
for (const agent of volatileAgents)
|
|
448
|
+
await this.agentMgr.addVolatileAgent(agent, group);
|
|
449
|
+
this.gatesByGroup.set(group, doc.qualityGates ?? []);
|
|
450
|
+
if (doc.checkpoints?.length)
|
|
451
|
+
await this.gating.setCheckpointDefinitions(group, doc.checkpoints);
|
|
452
|
+
if (doc.delays?.length)
|
|
453
|
+
await this.gating.setDelayDefinitions(group, doc.delays);
|
|
454
|
+
if (mission.status === "scheduled" || mission.status === "recurring")
|
|
455
|
+
this.scheduledOrigin.set(group, mission.status);
|
|
456
|
+
const actualIds = new Map(), created = [];
|
|
457
|
+
for (const task of tasks) {
|
|
458
|
+
const storedTask = await this.ctx.taskStore.createTask({ ...task, dependsOn: task.dependsOn.map(id => actualIds.get(id)) });
|
|
459
|
+
actualIds.set(task.id, storedTask.id);
|
|
460
|
+
created.push(storedTask);
|
|
461
|
+
this.taskMgr.notifyTaskCreated(storedTask);
|
|
462
|
+
}
|
|
463
|
+
await this.missions.updateMission(missionId, { status: "active", executionCount: sequence, ...(doc.notifications ? { notifications: doc.notifications } : {}) });
|
|
464
|
+
this.ctx.emitter.emit("mission:executed", { missionId, group, taskCount: created.length });
|
|
465
|
+
return { tasks: created, group };
|
|
342
466
|
}
|
|
343
|
-
|
|
344
|
-
//
|
|
345
|
-
|
|
467
|
+
catch (error) {
|
|
468
|
+
// Another caller may win after our initial lookup but before preflight.
|
|
469
|
+
// Its accepted receipt also wins over active-state/title/hook conflicts.
|
|
470
|
+
if (!admitted && persistence && options.idempotencyKey !== undefined) {
|
|
471
|
+
const accepted = await persistence.getMissionExecution(missionId, options.idempotencyKey);
|
|
472
|
+
if (accepted)
|
|
473
|
+
return { tasks: accepted.tasks, group: accepted.group, executionId: accepted.executionId };
|
|
474
|
+
}
|
|
475
|
+
throw error;
|
|
346
476
|
}
|
|
347
|
-
this.ctx.emitter.emit("mission:executed", { missionId, group, taskCount: tasks.length });
|
|
348
|
-
return { tasks, group };
|
|
349
477
|
}
|
|
350
478
|
/** Check if any mission groups have all tasks terminal, and clean up their volatile agents */
|
|
351
479
|
async cleanupCompletedGroups(tasks) {
|
|
352
480
|
const groups = new Set();
|
|
353
481
|
for (const t of tasks) {
|
|
354
|
-
|
|
355
|
-
|
|
482
|
+
const group = t.missionExecutionId ?? t.group;
|
|
483
|
+
if (group)
|
|
484
|
+
groups.add(group);
|
|
356
485
|
}
|
|
357
486
|
for (const group of groups) {
|
|
358
|
-
const groupTasks = tasks.filter(t => t.group === group);
|
|
487
|
+
const groupTasks = tasks.filter(t => (t.missionExecutionId ?? t.group) === group);
|
|
488
|
+
const label = groupTasks[0]?.group ?? group;
|
|
359
489
|
const allTerminal = groupTasks.every(t => t.status === "done" || t.status === "failed");
|
|
360
490
|
// If tasks went back to non-terminal (e.g. individual retry via retryTask),
|
|
361
491
|
// clear the cleaned flag so the group will be re-evaluated when done again.
|
|
@@ -367,18 +497,17 @@ export class MissionExecutor {
|
|
|
367
497
|
continue;
|
|
368
498
|
if (!allTerminal)
|
|
369
499
|
continue;
|
|
370
|
-
const
|
|
371
|
-
if (
|
|
372
|
-
await this.
|
|
373
|
-
}
|
|
374
|
-
this.cleanedGroups.add(group);
|
|
500
|
+
const capturedTask = groupTasks.find(task => task.missionExecutionId);
|
|
501
|
+
if (capturedTask)
|
|
502
|
+
await this.prepareTaskExecution(capturedTask);
|
|
375
503
|
// Auto-update mission status
|
|
376
504
|
const mission = await resolveMissionForGroup(this.missions, groupTasks, group);
|
|
377
|
-
|
|
505
|
+
const admission = capturedTask ? await resolveMissionExecutionStore(this.ctx)?.getMissionExecutionById(capturedTask.missionExecutionId) : undefined;
|
|
506
|
+
if (mission && mission.status === "active" && (!admission || admission.receipt.sequence === mission.executionCount)) {
|
|
378
507
|
let allDone = groupTasks.every(t => t.status === "done");
|
|
379
508
|
// Check mission quality threshold (only if all tasks passed structurally)
|
|
380
509
|
if (allDone && this.qualityCtrl) {
|
|
381
|
-
const thresholdResult = this.qualityCtrl.checkMissionThreshold(mission, groupTasks, this.ctx.config.settings.defaultQualityThreshold);
|
|
510
|
+
const thresholdResult = this.qualityCtrl.checkMissionThreshold(admission?.expected ?? mission, groupTasks, this.ctx.config.settings.defaultQualityThreshold);
|
|
382
511
|
if (!thresholdResult.passed) {
|
|
383
512
|
allDone = false; // Quality threshold not met — mark mission as failed
|
|
384
513
|
this.ctx.emitter.emit("log", {
|
|
@@ -402,17 +531,24 @@ export class MissionExecutor {
|
|
|
402
531
|
// Normal missions or successful one-shot scheduled missions
|
|
403
532
|
finalStatus = allDone ? "completed" : "failed";
|
|
404
533
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
534
|
+
const finalized = admission
|
|
535
|
+
? await resolveMissionExecutionStore(this.ctx).finalizeMissionExecution(admission.receipt.executionId, finalStatus)
|
|
536
|
+
: !!await this.missions.updateMission(mission.id, { status: finalStatus });
|
|
537
|
+
// A retry may have made tasks runnable since this cleanup snapshot.
|
|
538
|
+
// Keep definitions/agents and retry from fresh state on the next tick.
|
|
539
|
+
if (!finalized)
|
|
540
|
+
continue;
|
|
541
|
+
if (finalized) {
|
|
542
|
+
const report = await this.buildMissionReport(mission.id, label, groupTasks, allDone);
|
|
543
|
+
this.ctx.emitter.emit("mission:completed", { missionId: mission.id, group: label, allPassed: allDone, report });
|
|
544
|
+
this.qualityCtrl?.aggregateMissionMetrics(mission.id, groupTasks);
|
|
545
|
+
}
|
|
415
546
|
}
|
|
547
|
+
if ((this.ctx.config.settings.volatileCleanup ?? "on_complete") === "on_complete") {
|
|
548
|
+
await this.agentMgr.cleanupVolatileAgents(group);
|
|
549
|
+
}
|
|
550
|
+
await this.forgetExecution(group);
|
|
551
|
+
this.cleanedGroups.add(group);
|
|
416
552
|
}
|
|
417
553
|
}
|
|
418
554
|
// ═══════════════════════════════════════════════════════
|