Block phase progress without live backend
This commit is contained in:
1 parent
f502bfbd42
commit
49d0827650
2 files changed
+52
-3
No files matched your search
+28
-3
@@ -786,6 +786,7 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
public resume(): void {
|
public resume(): void {
|
||||||
if (!this.running || !this.paused) return;
|
if (!this.running || !this.paused) return;
|
||||||
this.paused = false;
|
this.paused = false;
|
||||||
|
this.stallReason = null;
|
||||||
this.emit('command:resumed');
|
this.emit('command:resumed');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,6 +838,9 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
/** Track whether we've seeded initial tasks for the current mission */
|
/** Track whether we've seeded initial tasks for the current mission */
|
||||||
private taskSeeded: boolean = false;
|
private taskSeeded: boolean = false;
|
||||||
|
|
||||||
|
/** Human-readable reason a mission was paused because required work failed. */
|
||||||
|
private stallReason: string | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main tick loop — seeds tasks, dispatches to operators, advances phases
|
* Main tick loop — seeds tasks, dispatches to operators, advances phases
|
||||||
*/
|
*/
|
||||||
@@ -889,13 +893,28 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
);
|
);
|
||||||
const inFlight = allMissionTasks.filter(t => this.activeDispatches.has(t.id));
|
const inFlight = allMissionTasks.filter(t => this.activeDispatches.has(t.id));
|
||||||
|
|
||||||
// If we have tasks, all are done, and nothing is in-flight → advance phase
|
// If we have tasks, all are done, and nothing is in-flight → advance phase.
|
||||||
|
// Failed required tasks are terminal, but they are not successful progress.
|
||||||
|
// Stall instead of walking the phase bar forward with no backend/model work.
|
||||||
if (allMissionTasks.length > 0 && pendingOrActive.length === 0 && inFlight.length === 0) {
|
if (allMissionTasks.length > 0 && pendingOrActive.length === 0 && inFlight.length === 0) {
|
||||||
|
const failedCurrentPhase = allMissionTasks.filter(
|
||||||
|
t => t.phase === mission.currentPhase && t.status === 'failed'
|
||||||
|
);
|
||||||
|
if (failedCurrentPhase.length > 0) {
|
||||||
|
const firstError = failedCurrentPhase[0].result?.error;
|
||||||
|
this.stallReason = `stalled in ${mission.currentPhase}: ${failedCurrentPhase.length} required task(s) failed` +
|
||||||
|
(firstError ? ` — ${firstError}` : '');
|
||||||
|
this.paused = true;
|
||||||
|
this.emit('command:paused');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const phaseIndex = mission.phases.indexOf(mission.currentPhase);
|
const phaseIndex = mission.phases.indexOf(mission.currentPhase);
|
||||||
if (phaseIndex === -1) return; // Guard: phase not found (race condition)
|
if (phaseIndex === -1) return; // Guard: phase not found (race condition)
|
||||||
if (phaseIndex < mission.phases.length - 1) {
|
if (phaseIndex < mission.phases.length - 1) {
|
||||||
// Advance to next phase and generate tasks
|
// Advance to next phase and generate tasks
|
||||||
this.mission.advancePhase(mission.id);
|
this.mission.advancePhase(mission.id);
|
||||||
|
this.stallReason = null;
|
||||||
const targets = this.targetEnv.getAllTargets();
|
const targets = this.targetEnv.getAllTargets();
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
this.mission.generateNextPhaseTasks(target.address);
|
this.mission.generateNextPhaseTasks(target.address);
|
||||||
@@ -972,8 +991,12 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
// double-fire the completion hook.
|
// double-fire the completion hook.
|
||||||
if (!this.activeDispatches.has(task.id)) return;
|
if (!this.activeDispatches.has(task.id)) return;
|
||||||
this.clearDispatch(task.id);
|
this.clearDispatch(task.id);
|
||||||
taskQueue.complete(task.id, result);
|
if (result.success === false) {
|
||||||
this.hooks.onTaskCompleted?.(task);
|
taskQueue.fail(task.id, result.error || result.output || 'task returned unsuccessful result');
|
||||||
|
} else {
|
||||||
|
taskQueue.complete(task.id, result);
|
||||||
|
this.hooks.onTaskCompleted?.(task);
|
||||||
|
}
|
||||||
}).catch((_error) => {
|
}).catch((_error) => {
|
||||||
if (!this.activeDispatches.has(task.id)) return;
|
if (!this.activeDispatches.has(task.id)) return;
|
||||||
this.clearDispatch(task.id);
|
this.clearDispatch(task.id);
|
||||||
@@ -1199,6 +1222,7 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
vault: ReturnType<EvidenceVault['getStats']>;
|
vault: ReturnType<EvidenceVault['getStats']>;
|
||||||
opsec: ReturnType<OpsecController['getStats']>;
|
opsec: ReturnType<OpsecController['getStats']>;
|
||||||
activeMission: string | null;
|
activeMission: string | null;
|
||||||
|
stallReason: string | null;
|
||||||
} {
|
} {
|
||||||
const activeMission = this.mission.getActiveMission();
|
const activeMission = this.mission.getActiveMission();
|
||||||
|
|
||||||
@@ -1212,6 +1236,7 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
vault: this.vault.getStats(),
|
vault: this.vault.getStats(),
|
||||||
opsec: this.opsec.getStats(),
|
opsec: this.opsec.getStats(),
|
||||||
activeMission: activeMission?.id || null,
|
activeMission: activeMission?.id || null,
|
||||||
|
stallReason: this.stallReason,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5982,6 +5982,13 @@ app.post('/api/mission/start', async (req: Request, res: Response): Promise<void
|
|||||||
res.status(400).json({ error: 'API key required — pass apiKey, configure one on the server, or connect a local agent (Claude Code / Codex / Hermes)' });
|
res.status(400).json({ error: 'API key required — pass apiKey, configure one on the server, or connect a local agent (Claude Code / Codex / Hermes)' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (provider === 'local-agent') {
|
||||||
|
const localAgent = await requireLiveLocalAgent(model);
|
||||||
|
if (!localAgent.ok) {
|
||||||
|
res.status(503).json({ error: localAgent.error });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (targets.length === 0) {
|
if (targets.length === 0) {
|
||||||
res.status(400).json({ error: 'At least one target required' });
|
res.status(400).json({ error: 'At least one target required' });
|
||||||
@@ -6217,6 +6224,7 @@ app.get('/api/mission/status', (_req: Request, res: Response) => {
|
|||||||
res.json({
|
res.json({
|
||||||
active: status.running,
|
active: status.running,
|
||||||
paused: status.paused,
|
paused: status.paused,
|
||||||
|
stallReason: status.stallReason,
|
||||||
name: status.name,
|
name: status.name,
|
||||||
tickCount: status.tickCount,
|
tickCount: status.tickCount,
|
||||||
mission: mission ? {
|
mission: mission ? {
|
||||||
@@ -7380,6 +7388,22 @@ async function refreshConnectedLocalAgentHealth(force = false): Promise<void> {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function requireLiveLocalAgent(model: string | undefined): Promise<{ ok: true; id: string } | { ok: false; error: string }> {
|
||||||
|
const id = String(model || 'codex').trim();
|
||||||
|
const entry = connectedLocalAgents.get(id);
|
||||||
|
if (!entry) return { ok: false, error: `local agent "${id}" is not connected — connect it in Settings first` };
|
||||||
|
|
||||||
|
await refreshConnectedLocalAgentHealth(true);
|
||||||
|
const refreshed = connectedLocalAgents.get(id);
|
||||||
|
if (refreshed?.lastPing?.ok) return { ok: true, id };
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `local agent "${id}" is connected but not live` +
|
||||||
|
(refreshed?.lastPing?.error ? ` — ${refreshed.lastPing.error}` : ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// GET /api/agents/local/detect — which agents are installed / authed / ready (no tokens spent)
|
// GET /api/agents/local/detect — which agents are installed / authed / ready (no tokens spent)
|
||||||
app.get('/api/agents/local/detect', async (_req: Request, res: Response): Promise<void> => {
|
app.get('/api/agents/local/detect', async (_req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user