refactor: split paired executor lifecycle (#187)
This commit is contained in:
@@ -118,10 +118,6 @@
|
|||||||
"maxFunctionLines": 417,
|
"maxFunctionLines": 417,
|
||||||
"owner": "message executor lifecycle hotspot"
|
"owner": "message executor lifecycle hotspot"
|
||||||
},
|
},
|
||||||
"src/message-agent-executor-paired.ts": {
|
|
||||||
"maxFunctionLines": 440,
|
|
||||||
"owner": "message executor paired hotspot"
|
|
||||||
},
|
|
||||||
"src/message-agent-executor-target.ts": {
|
"src/message-agent-executor-target.ts": {
|
||||||
"maxFunctionLines": 274,
|
"maxFunctionLines": 274,
|
||||||
"maxComplexity": 48,
|
"maxComplexity": 48,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export interface PairedExecutionLifecycle {
|
|||||||
asyncFinalize(): Promise<void>;
|
asyncFinalize(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPairedExecutionLifecycle(args: {
|
interface CreatePairedExecutionLifecycleArgs {
|
||||||
pairedExecutionContext?: PreparedPairedExecutionContext;
|
pairedExecutionContext?: PreparedPairedExecutionContext;
|
||||||
pairedTurnIdentity?: PairedTurnIdentity;
|
pairedTurnIdentity?: PairedTurnIdentity;
|
||||||
completedRole: PairedRoomRole;
|
completedRole: PairedRoomRole;
|
||||||
@@ -109,38 +109,138 @@ export function createPairedExecutionLifecycle(args: {
|
|||||||
getCloseReason?: () => string | null;
|
getCloseReason?: () => string | null;
|
||||||
onOutput?: (output: AgentOutput) => Promise<void>;
|
onOutput?: (output: AgentOutput) => Promise<void>;
|
||||||
log: ExecutorLog;
|
log: ExecutorLog;
|
||||||
}): PairedExecutionLifecycle {
|
}
|
||||||
const {
|
|
||||||
pairedExecutionContext,
|
|
||||||
pairedTurnIdentity,
|
|
||||||
completedRole,
|
|
||||||
chatJid,
|
|
||||||
runId,
|
|
||||||
enqueueMessageCheck,
|
|
||||||
getDirectTerminalDeliveryText,
|
|
||||||
getCloseReason,
|
|
||||||
onOutput,
|
|
||||||
log,
|
|
||||||
} = args;
|
|
||||||
|
|
||||||
let pairedExecutionStatus: 'succeeded' | 'failed' = 'failed';
|
type PairedExecutionStatus = 'succeeded' | 'failed';
|
||||||
let pairedExecutionSummary: string | null = null;
|
|
||||||
let pairedFinalOutput: string | null = null;
|
|
||||||
let pairedSummaryLocked = false;
|
|
||||||
let pairedExecutionCompleted = false;
|
|
||||||
let pairedExecutionDelegated = false;
|
|
||||||
let pairedSawOutput = false;
|
|
||||||
let pairedTurnOutputPersisted = false;
|
|
||||||
let pairedTurnStateFinalized = false;
|
|
||||||
let leaseHeartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
||||||
const requiresVisibleVerdict =
|
|
||||||
pairedExecutionContext?.requiresVisibleVerdict === true;
|
|
||||||
const missingVisibleVerdictSummary =
|
|
||||||
'Execution completed without a visible terminal verdict.';
|
|
||||||
const wasInterruptedByHumanMessage = (): boolean =>
|
|
||||||
isHumanMessageCloseReason(getCloseReason?.() ?? null);
|
|
||||||
|
|
||||||
const currentRunOwnsActiveAttempt = (reason: string): boolean => {
|
interface FinalizeState {
|
||||||
|
directTerminalOutput: string | null;
|
||||||
|
effectiveStatus: PairedExecutionStatus;
|
||||||
|
sawOutputForFollowUp: boolean;
|
||||||
|
interruptedByHumanMessage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PairedExecutionLifecycleController implements PairedExecutionLifecycle {
|
||||||
|
private pairedExecutionStatus: PairedExecutionStatus = 'failed';
|
||||||
|
private pairedExecutionSummary: string | null = null;
|
||||||
|
private pairedFinalOutput: string | null = null;
|
||||||
|
private pairedSummaryLocked = false;
|
||||||
|
private pairedExecutionCompleted = false;
|
||||||
|
private pairedExecutionDelegated = false;
|
||||||
|
private pairedSawOutput = false;
|
||||||
|
private pairedTurnOutputPersisted = false;
|
||||||
|
private pairedTurnStateFinalized = false;
|
||||||
|
private leaseHeartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
constructor(private readonly args: CreatePairedExecutionLifecycleArgs) {
|
||||||
|
if (!this.args.pairedExecutionContext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.leaseHeartbeatTimer = setInterval(
|
||||||
|
() => this.heartbeatLeaseIfNeeded(),
|
||||||
|
PAIRED_TASK_EXECUTION_LEASE_HEARTBEAT_MS,
|
||||||
|
);
|
||||||
|
this.leaseHeartbeatTimer.unref?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSummary({
|
||||||
|
outputText,
|
||||||
|
errorText,
|
||||||
|
}: {
|
||||||
|
outputText?: string | null;
|
||||||
|
errorText?: string | null;
|
||||||
|
}): void {
|
||||||
|
if (this.pairedSummaryLocked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputText && outputText.length > 0) {
|
||||||
|
this.pairedExecutionSummary = outputText.slice(0, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorText && errorText.length > 0) {
|
||||||
|
this.pairedExecutionSummary = errorText.slice(0, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recordFinalOutputBeforeDelivery(outputText: string): boolean {
|
||||||
|
if (this.wasInterruptedByHumanMessage()) return false;
|
||||||
|
if (!this.currentRunOwnsActiveAttempt('streamed-final-output')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.lockVisibleVerdict(outputText);
|
||||||
|
this.completeSuccessfulOwnerTurnBeforeDeliveryIfNeeded();
|
||||||
|
this.persistPairedTurnOutputIfNeeded();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
completeImmediately({ status }: { status: PairedExecutionStatus }): void {
|
||||||
|
const { completedRole, pairedExecutionContext, runId } = this.args;
|
||||||
|
if (!pairedExecutionContext || this.pairedExecutionCompleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pairedExecutionStatus = status;
|
||||||
|
if (status === 'succeeded') {
|
||||||
|
this.persistPairedTurnOutputIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearLeaseHeartbeat();
|
||||||
|
completePairedExecutionContext({
|
||||||
|
taskId: pairedExecutionContext.task.id,
|
||||||
|
role: completedRole,
|
||||||
|
status,
|
||||||
|
runId,
|
||||||
|
summary: this.pairedExecutionSummary,
|
||||||
|
});
|
||||||
|
this.pairedExecutionCompleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
markDelegated(): void {
|
||||||
|
this.pairedExecutionDelegated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
markStatus(status: PairedExecutionStatus): void {
|
||||||
|
this.pairedExecutionStatus = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
markSawOutput(sawOutput: boolean): void {
|
||||||
|
this.pairedSawOutput = sawOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSummary(): string | null {
|
||||||
|
return this.pairedExecutionSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
async asyncFinalize(): Promise<void> {
|
||||||
|
this.clearLeaseHeartbeat();
|
||||||
|
|
||||||
|
if (!this.currentRunOwnsActiveAttempt('async-finalize')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.releaseDelegatedExecutionIfNeeded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = this.resolveFinalizeState();
|
||||||
|
this.completeStoredExecutionIfNeeded(state);
|
||||||
|
this.finalizePairedTurnState(
|
||||||
|
state.effectiveStatus,
|
||||||
|
state.effectiveStatus === 'failed' ? this.pairedExecutionSummary : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.notifyCompletionAndQueueFollowUp(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private wasInterruptedByHumanMessage(): boolean {
|
||||||
|
return isHumanMessageCloseReason(this.args.getCloseReason?.() ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private currentRunOwnsActiveAttempt(reason: string): boolean {
|
||||||
|
const { log, pairedExecutionContext, pairedTurnIdentity, runId } =
|
||||||
|
this.args;
|
||||||
if (!pairedTurnIdentity) {
|
if (!pairedTurnIdentity) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -176,13 +276,14 @@ export function createPairedExecutionLifecycle(args: {
|
|||||||
'Skipping paired final side effects because this run no longer owns the active attempt',
|
'Skipping paired final side effects because this run no longer owns the active attempt',
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
|
|
||||||
const finalizePairedTurnState = (
|
private finalizePairedTurnState(
|
||||||
status: 'succeeded' | 'failed',
|
status: 'succeeded' | 'failed',
|
||||||
errorText?: string | null,
|
errorText?: string | null,
|
||||||
) => {
|
): void {
|
||||||
if (!pairedTurnIdentity || pairedTurnStateFinalized) {
|
const { pairedTurnIdentity } = this.args;
|
||||||
|
if (!pairedTurnIdentity || this.pairedTurnStateFinalized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status === 'succeeded') {
|
if (status === 'succeeded') {
|
||||||
@@ -190,20 +291,22 @@ export function createPairedExecutionLifecycle(args: {
|
|||||||
} else {
|
} else {
|
||||||
failPairedTurn({
|
failPairedTurn({
|
||||||
turnIdentity: pairedTurnIdentity,
|
turnIdentity: pairedTurnIdentity,
|
||||||
error: errorText ?? pairedExecutionSummary,
|
error: errorText ?? this.pairedExecutionSummary,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
pairedTurnStateFinalized = true;
|
this.pairedTurnStateFinalized = true;
|
||||||
};
|
}
|
||||||
|
|
||||||
const clearLeaseHeartbeat = () => {
|
private clearLeaseHeartbeat(): void {
|
||||||
if (!leaseHeartbeatTimer) {
|
if (!this.leaseHeartbeatTimer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearInterval(leaseHeartbeatTimer);
|
clearInterval(this.leaseHeartbeatTimer);
|
||||||
leaseHeartbeatTimer = null;
|
this.leaseHeartbeatTimer = null;
|
||||||
};
|
}
|
||||||
const heartbeatLeaseIfNeeded = () => {
|
|
||||||
|
private heartbeatLeaseIfNeeded(): void {
|
||||||
|
const { pairedExecutionContext, runId, log } = this.args;
|
||||||
if (!pairedExecutionContext) {
|
if (!pairedExecutionContext) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -231,22 +334,15 @@ export function createPairedExecutionLifecycle(args: {
|
|||||||
'Failed to refresh paired execution lease heartbeat',
|
'Failed to refresh paired execution lease heartbeat',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (pairedExecutionContext) {
|
|
||||||
leaseHeartbeatTimer = setInterval(
|
|
||||||
heartbeatLeaseIfNeeded,
|
|
||||||
PAIRED_TASK_EXECUTION_LEASE_HEARTBEAT_MS,
|
|
||||||
);
|
|
||||||
leaseHeartbeatTimer.unref?.();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const persistPairedTurnOutputIfNeeded = () => {
|
private persistPairedTurnOutputIfNeeded(): void {
|
||||||
|
const { completedRole, pairedExecutionContext } = this.args;
|
||||||
if (
|
if (
|
||||||
!pairedExecutionContext ||
|
!pairedExecutionContext ||
|
||||||
pairedTurnOutputPersisted ||
|
this.pairedTurnOutputPersisted ||
|
||||||
!pairedFinalOutput ||
|
!this.pairedFinalOutput ||
|
||||||
pairedFinalOutput.length === 0
|
this.pairedFinalOutput.length === 0
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -256,57 +352,65 @@ export function createPairedExecutionLifecycle(args: {
|
|||||||
pairedExecutionContext.task.id,
|
pairedExecutionContext.task.id,
|
||||||
turnNumber,
|
turnNumber,
|
||||||
completedRole,
|
completedRole,
|
||||||
pairedFinalOutput,
|
this.pairedFinalOutput,
|
||||||
);
|
);
|
||||||
pairedTurnOutputPersisted = true;
|
this.pairedTurnOutputPersisted = true;
|
||||||
};
|
}
|
||||||
|
|
||||||
const completeSuccessfulOwnerTurnBeforeDeliveryIfNeeded = () => {
|
private completeSuccessfulOwnerTurnBeforeDeliveryIfNeeded(): void {
|
||||||
|
const { completedRole, pairedExecutionContext, runId } = this.args;
|
||||||
if (
|
if (
|
||||||
completedRole !== 'owner' ||
|
completedRole !== 'owner' ||
|
||||||
!pairedExecutionContext ||
|
!pairedExecutionContext ||
|
||||||
pairedExecutionCompleted ||
|
this.pairedExecutionCompleted ||
|
||||||
!pairedFinalOutput ||
|
!this.pairedFinalOutput ||
|
||||||
pairedFinalOutput.length === 0
|
this.pairedFinalOutput.length === 0
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pairedExecutionStatus = 'succeeded';
|
this.pairedExecutionStatus = 'succeeded';
|
||||||
pairedSawOutput = true;
|
this.pairedSawOutput = true;
|
||||||
persistPairedTurnOutputIfNeeded();
|
this.persistPairedTurnOutputIfNeeded();
|
||||||
clearLeaseHeartbeat();
|
this.clearLeaseHeartbeat();
|
||||||
completePairedExecutionContext({
|
completePairedExecutionContext({
|
||||||
taskId: pairedExecutionContext.task.id,
|
taskId: pairedExecutionContext.task.id,
|
||||||
role: completedRole,
|
role: completedRole,
|
||||||
status: 'succeeded',
|
status: 'succeeded',
|
||||||
runId,
|
runId,
|
||||||
summary: pairedExecutionSummary,
|
summary: this.pairedExecutionSummary,
|
||||||
});
|
});
|
||||||
pairedExecutionCompleted = true;
|
this.pairedExecutionCompleted = true;
|
||||||
};
|
}
|
||||||
|
|
||||||
const lockVisibleVerdict = (outputText: string) => {
|
private lockVisibleVerdict(outputText: string): void {
|
||||||
if (outputText.length === 0) {
|
if (outputText.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!pairedFinalOutput || pairedFinalOutput.length === 0) {
|
if (!this.pairedFinalOutput || this.pairedFinalOutput.length === 0) {
|
||||||
pairedFinalOutput = outputText;
|
this.pairedFinalOutput = outputText;
|
||||||
}
|
}
|
||||||
if (!pairedSummaryLocked) {
|
if (!this.pairedSummaryLocked) {
|
||||||
pairedExecutionSummary = outputText.slice(0, 500);
|
this.pairedExecutionSummary = outputText.slice(0, 500);
|
||||||
pairedSummaryLocked = true;
|
this.pairedSummaryLocked = true;
|
||||||
}
|
}
|
||||||
pairedSawOutput = true;
|
this.pairedSawOutput = true;
|
||||||
};
|
}
|
||||||
|
|
||||||
const adoptDirectTerminalDeliveryIfNeeded = () => {
|
private adoptDirectTerminalDeliveryIfNeeded(): string | null {
|
||||||
|
const {
|
||||||
|
completedRole,
|
||||||
|
getDirectTerminalDeliveryText,
|
||||||
|
log,
|
||||||
|
pairedExecutionContext,
|
||||||
|
runId,
|
||||||
|
} = this.args;
|
||||||
const outputText = getDirectTerminalDeliveryText?.();
|
const outputText = getDirectTerminalDeliveryText?.();
|
||||||
if (!outputText || outputText.length === 0) {
|
if (!outputText || outputText.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!pairedFinalOutput || pairedFinalOutput.length === 0) {
|
if (!this.pairedFinalOutput || this.pairedFinalOutput.length === 0) {
|
||||||
lockVisibleVerdict(outputText);
|
this.lockVisibleVerdict(outputText);
|
||||||
log.info(
|
log.info(
|
||||||
{
|
{
|
||||||
pairedTaskId: pairedExecutionContext?.task.id ?? null,
|
pairedTaskId: pairedExecutionContext?.task.id ?? null,
|
||||||
@@ -315,226 +419,230 @@ export function createPairedExecutionLifecycle(args: {
|
|||||||
},
|
},
|
||||||
'Adopted direct terminal delivery as paired final output',
|
'Adopted direct terminal delivery as paired final output',
|
||||||
);
|
);
|
||||||
} else if (!pairedSummaryLocked) {
|
} else if (!this.pairedSummaryLocked) {
|
||||||
pairedExecutionSummary = pairedFinalOutput.slice(0, 500);
|
this.pairedExecutionSummary = this.pairedFinalOutput.slice(0, 500);
|
||||||
pairedSummaryLocked = true;
|
this.pairedSummaryLocked = true;
|
||||||
}
|
}
|
||||||
return outputText;
|
return outputText;
|
||||||
};
|
}
|
||||||
|
|
||||||
return {
|
private releaseDelegatedExecutionIfNeeded(): boolean {
|
||||||
updateSummary({ outputText, errorText }) {
|
const { log, pairedExecutionContext, runId } = this.args;
|
||||||
if (pairedSummaryLocked) {
|
if (!pairedExecutionContext || !this.pairedExecutionDelegated) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
if (outputText && outputText.length > 0) {
|
releasePairedTaskExecutionLease({
|
||||||
pairedExecutionSummary = outputText.slice(0, 500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorText && errorText.length > 0) {
|
|
||||||
pairedExecutionSummary = errorText.slice(0, 500);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
recordFinalOutputBeforeDelivery(outputText) {
|
|
||||||
if (wasInterruptedByHumanMessage()) return false;
|
|
||||||
if (!currentRunOwnsActiveAttempt('streamed-final-output')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
lockVisibleVerdict(outputText);
|
|
||||||
completeSuccessfulOwnerTurnBeforeDeliveryIfNeeded();
|
|
||||||
persistPairedTurnOutputIfNeeded();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
completeImmediately({ status }) {
|
|
||||||
if (!pairedExecutionContext || pairedExecutionCompleted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pairedExecutionStatus = status;
|
|
||||||
if (status === 'succeeded') {
|
|
||||||
persistPairedTurnOutputIfNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearLeaseHeartbeat();
|
|
||||||
completePairedExecutionContext({
|
|
||||||
taskId: pairedExecutionContext.task.id,
|
taskId: pairedExecutionContext.task.id,
|
||||||
role: completedRole,
|
|
||||||
status,
|
|
||||||
runId,
|
runId,
|
||||||
summary: pairedExecutionSummary,
|
|
||||||
});
|
});
|
||||||
pairedExecutionCompleted = true;
|
} catch (err) {
|
||||||
},
|
log.warn(
|
||||||
|
|
||||||
markDelegated() {
|
|
||||||
pairedExecutionDelegated = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
markStatus(status) {
|
|
||||||
pairedExecutionStatus = status;
|
|
||||||
},
|
|
||||||
|
|
||||||
markSawOutput(sawOutput) {
|
|
||||||
pairedSawOutput = sawOutput;
|
|
||||||
},
|
|
||||||
|
|
||||||
getSummary() {
|
|
||||||
return pairedExecutionSummary;
|
|
||||||
},
|
|
||||||
|
|
||||||
async asyncFinalize() {
|
|
||||||
clearLeaseHeartbeat();
|
|
||||||
|
|
||||||
if (!currentRunOwnsActiveAttempt('async-finalize')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pairedExecutionContext && pairedExecutionDelegated) {
|
|
||||||
try {
|
|
||||||
releasePairedTaskExecutionLease({
|
|
||||||
taskId: pairedExecutionContext.task.id,
|
|
||||||
runId,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
log.warn(
|
|
||||||
{
|
|
||||||
pairedTaskId: pairedExecutionContext.task.id,
|
|
||||||
runId,
|
|
||||||
err,
|
|
||||||
},
|
|
||||||
'Failed to release paired execution lease for delegated fallback handoff',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
pairedExecutionCompleted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const directTerminalOutput = adoptDirectTerminalDeliveryIfNeeded();
|
|
||||||
|
|
||||||
const missingVisibleVerdict =
|
|
||||||
requiresVisibleVerdict &&
|
|
||||||
(!pairedFinalOutput || pairedFinalOutput.length === 0);
|
|
||||||
if (missingVisibleVerdict) {
|
|
||||||
pairedExecutionSummary = missingVisibleVerdictSummary;
|
|
||||||
log.warn(
|
|
||||||
{
|
|
||||||
pairedTaskId: pairedExecutionContext?.task.id ?? null,
|
|
||||||
role: completedRole,
|
|
||||||
runId,
|
|
||||||
},
|
|
||||||
'Treating paired execution as failed because it ended without a visible terminal verdict',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const effectiveStatus =
|
|
||||||
completedRole === 'owner' &&
|
|
||||||
pairedExecutionStatus === 'succeeded' &&
|
|
||||||
!pairedSawOutput
|
|
||||||
? 'failed'
|
|
||||||
: missingVisibleVerdict && pairedExecutionStatus === 'succeeded'
|
|
||||||
? 'failed'
|
|
||||||
: pairedExecutionStatus;
|
|
||||||
const sawOutputForFollowUp = missingVisibleVerdict
|
|
||||||
? false
|
|
||||||
: pairedSawOutput;
|
|
||||||
const interruptedByHumanMessage = wasInterruptedByHumanMessage();
|
|
||||||
|
|
||||||
if (pairedExecutionContext && !pairedExecutionCompleted) {
|
|
||||||
if (interruptedByHumanMessage) {
|
|
||||||
releaseInterruptedPairedExecution(
|
|
||||||
pairedExecutionContext.task.id,
|
|
||||||
runId,
|
|
||||||
log,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (effectiveStatus === 'succeeded') {
|
|
||||||
try {
|
|
||||||
persistPairedTurnOutputIfNeeded();
|
|
||||||
} catch (err) {
|
|
||||||
log.warn(
|
|
||||||
{ pairedTaskId: pairedExecutionContext.task.id, err },
|
|
||||||
'Failed to store paired turn output',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completeStoredExecution(
|
|
||||||
pairedExecutionContext.task.id,
|
|
||||||
completedRole,
|
|
||||||
effectiveStatus,
|
|
||||||
runId,
|
|
||||||
pairedExecutionSummary,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
pairedExecutionCompleted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
finalizePairedTurnState(
|
|
||||||
effectiveStatus,
|
|
||||||
effectiveStatus === 'failed' ? pairedExecutionSummary : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!pairedExecutionContext) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (interruptedByHumanMessage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const finishedTask = getPairedTaskById(pairedExecutionContext.task.id);
|
|
||||||
await notifyPairedCompletionIfNeeded({
|
|
||||||
task: finishedTask,
|
|
||||||
chatJid,
|
|
||||||
onOutput,
|
|
||||||
});
|
|
||||||
|
|
||||||
const queueAction =
|
|
||||||
directTerminalOutput &&
|
|
||||||
(completedRole === 'reviewer' || completedRole === 'arbiter')
|
|
||||||
? 'none'
|
|
||||||
: resolvePairedFollowUpQueueAction({
|
|
||||||
completedRole,
|
|
||||||
executionStatus: effectiveStatus,
|
|
||||||
sawOutput: sawOutputForFollowUp,
|
|
||||||
taskStatus: finishedTask?.status ?? null,
|
|
||||||
outputSummary: pairedExecutionSummary,
|
|
||||||
});
|
|
||||||
if (queueAction !== 'pending' || !finishedTask) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const followUpResult = enqueuePairedFollowUpAfterEvent({
|
|
||||||
chatJid,
|
|
||||||
runId,
|
|
||||||
task: finishedTask,
|
|
||||||
source: 'executor-recovery',
|
|
||||||
completedRole,
|
|
||||||
executionStatus: effectiveStatus,
|
|
||||||
sawOutput: sawOutputForFollowUp,
|
|
||||||
fallbackLastTurnOutputRole: sawOutputForFollowUp ? completedRole : null,
|
|
||||||
fallbackLastTurnOutputVerdict:
|
|
||||||
sawOutputForFollowUp && pairedExecutionSummary
|
|
||||||
? parseVisibleVerdict(pairedExecutionSummary)
|
|
||||||
: null,
|
|
||||||
enqueueMessageCheck,
|
|
||||||
});
|
|
||||||
if (followUpResult.kind !== 'paired-follow-up') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.info(
|
|
||||||
{
|
{
|
||||||
taskId: pairedExecutionContext.task.id,
|
pairedTaskId: pairedExecutionContext.task.id,
|
||||||
role: completedRole,
|
runId,
|
||||||
pairedExecutionStatus: effectiveStatus,
|
err,
|
||||||
taskStatus: finishedTask.status,
|
|
||||||
intentKind: followUpResult.intentKind,
|
|
||||||
scheduled: followUpResult.scheduled,
|
|
||||||
},
|
},
|
||||||
followUpResult.scheduled
|
'Failed to release paired execution lease for delegated fallback handoff',
|
||||||
? 'Queued paired follow-up after failed reviewer/arbiter execution left a pending task state'
|
|
||||||
: 'Skipped duplicate paired follow-up after failed reviewer/arbiter execution while task state was unchanged',
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
};
|
this.pairedExecutionCompleted = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveFinalizeState(): FinalizeState {
|
||||||
|
const directTerminalOutput = this.adoptDirectTerminalDeliveryIfNeeded();
|
||||||
|
const missingVisibleVerdict = this.resolveMissingVisibleVerdict();
|
||||||
|
const effectiveStatus = this.resolveEffectiveStatus(missingVisibleVerdict);
|
||||||
|
const sawOutputForFollowUp = missingVisibleVerdict
|
||||||
|
? false
|
||||||
|
: this.pairedSawOutput;
|
||||||
|
|
||||||
|
return {
|
||||||
|
directTerminalOutput,
|
||||||
|
effectiveStatus,
|
||||||
|
sawOutputForFollowUp,
|
||||||
|
interruptedByHumanMessage: this.wasInterruptedByHumanMessage(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveMissingVisibleVerdict(): boolean {
|
||||||
|
const { completedRole, log, pairedExecutionContext, runId } = this.args;
|
||||||
|
const missingVisibleVerdict =
|
||||||
|
pairedExecutionContext?.requiresVisibleVerdict === true &&
|
||||||
|
(!this.pairedFinalOutput || this.pairedFinalOutput.length === 0);
|
||||||
|
if (!missingVisibleVerdict) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.pairedExecutionSummary =
|
||||||
|
'Execution completed without a visible terminal verdict.';
|
||||||
|
log.warn(
|
||||||
|
{
|
||||||
|
pairedTaskId: pairedExecutionContext?.task.id ?? null,
|
||||||
|
role: completedRole,
|
||||||
|
runId,
|
||||||
|
},
|
||||||
|
'Treating paired execution as failed because it ended without a visible terminal verdict',
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveEffectiveStatus(
|
||||||
|
missingVisibleVerdict: boolean,
|
||||||
|
): PairedExecutionStatus {
|
||||||
|
if (
|
||||||
|
this.args.completedRole === 'owner' &&
|
||||||
|
this.pairedExecutionStatus === 'succeeded' &&
|
||||||
|
!this.pairedSawOutput
|
||||||
|
) {
|
||||||
|
return 'failed';
|
||||||
|
}
|
||||||
|
if (missingVisibleVerdict && this.pairedExecutionStatus === 'succeeded') {
|
||||||
|
return 'failed';
|
||||||
|
}
|
||||||
|
return this.pairedExecutionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
private completeStoredExecutionIfNeeded(state: FinalizeState): void {
|
||||||
|
const { completedRole, log, pairedExecutionContext, runId } = this.args;
|
||||||
|
if (!pairedExecutionContext || this.pairedExecutionCompleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.interruptedByHumanMessage) {
|
||||||
|
releaseInterruptedPairedExecution(
|
||||||
|
pairedExecutionContext.task.id,
|
||||||
|
runId,
|
||||||
|
log,
|
||||||
|
);
|
||||||
|
this.pairedExecutionCompleted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.persistSuccessfulOutputBeforeCompletion(state.effectiveStatus);
|
||||||
|
completeStoredExecution(
|
||||||
|
pairedExecutionContext.task.id,
|
||||||
|
completedRole,
|
||||||
|
state.effectiveStatus,
|
||||||
|
runId,
|
||||||
|
this.pairedExecutionSummary,
|
||||||
|
);
|
||||||
|
this.pairedExecutionCompleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private persistSuccessfulOutputBeforeCompletion(
|
||||||
|
effectiveStatus: PairedExecutionStatus,
|
||||||
|
): void {
|
||||||
|
if (effectiveStatus !== 'succeeded') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.persistPairedTurnOutputIfNeeded();
|
||||||
|
} catch (err) {
|
||||||
|
this.args.log.warn(
|
||||||
|
{ pairedTaskId: this.args.pairedExecutionContext?.task.id, err },
|
||||||
|
'Failed to store paired turn output',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async notifyCompletionAndQueueFollowUp(
|
||||||
|
state: FinalizeState,
|
||||||
|
): Promise<void> {
|
||||||
|
const { chatJid, onOutput, pairedExecutionContext } = this.args;
|
||||||
|
if (!pairedExecutionContext || state.interruptedByHumanMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const finishedTask = getPairedTaskById(pairedExecutionContext.task.id);
|
||||||
|
await notifyPairedCompletionIfNeeded({
|
||||||
|
task: finishedTask,
|
||||||
|
chatJid,
|
||||||
|
onOutput,
|
||||||
|
});
|
||||||
|
this.queueFollowUpIfNeeded(state, finishedTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private queueFollowUpIfNeeded(
|
||||||
|
state: FinalizeState,
|
||||||
|
finishedTask: PairedTaskRecord | null | undefined,
|
||||||
|
): void {
|
||||||
|
const queueAction = this.resolveQueueAction(state, finishedTask);
|
||||||
|
if (queueAction !== 'pending' || !finishedTask) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const followUpResult = this.enqueueFollowUp(state, finishedTask);
|
||||||
|
this.logFollowUpResult(followUpResult, state, finishedTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveQueueAction(
|
||||||
|
state: FinalizeState,
|
||||||
|
finishedTask: PairedTaskRecord | null | undefined,
|
||||||
|
): ReturnType<typeof resolvePairedFollowUpQueueAction> | 'none' {
|
||||||
|
const { completedRole } = this.args;
|
||||||
|
if (
|
||||||
|
state.directTerminalOutput &&
|
||||||
|
(completedRole === 'reviewer' || completedRole === 'arbiter')
|
||||||
|
) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
return resolvePairedFollowUpQueueAction({
|
||||||
|
completedRole,
|
||||||
|
executionStatus: state.effectiveStatus,
|
||||||
|
sawOutput: state.sawOutputForFollowUp,
|
||||||
|
taskStatus: finishedTask?.status ?? null,
|
||||||
|
outputSummary: this.pairedExecutionSummary,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private enqueueFollowUp(
|
||||||
|
state: FinalizeState,
|
||||||
|
finishedTask: PairedTaskRecord,
|
||||||
|
): ReturnType<typeof enqueuePairedFollowUpAfterEvent> {
|
||||||
|
const { chatJid, completedRole, enqueueMessageCheck, runId } = this.args;
|
||||||
|
return enqueuePairedFollowUpAfterEvent({
|
||||||
|
chatJid,
|
||||||
|
runId,
|
||||||
|
task: finishedTask,
|
||||||
|
source: 'executor-recovery',
|
||||||
|
completedRole,
|
||||||
|
executionStatus: state.effectiveStatus,
|
||||||
|
sawOutput: state.sawOutputForFollowUp,
|
||||||
|
fallbackLastTurnOutputRole: state.sawOutputForFollowUp
|
||||||
|
? completedRole
|
||||||
|
: null,
|
||||||
|
fallbackLastTurnOutputVerdict:
|
||||||
|
state.sawOutputForFollowUp && this.pairedExecutionSummary
|
||||||
|
? parseVisibleVerdict(this.pairedExecutionSummary)
|
||||||
|
: null,
|
||||||
|
enqueueMessageCheck,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private logFollowUpResult(
|
||||||
|
followUpResult: ReturnType<typeof enqueuePairedFollowUpAfterEvent>,
|
||||||
|
state: FinalizeState,
|
||||||
|
finishedTask: PairedTaskRecord,
|
||||||
|
): void {
|
||||||
|
const { completedRole, log, pairedExecutionContext } = this.args;
|
||||||
|
if (followUpResult.kind !== 'paired-follow-up') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.info(
|
||||||
|
{
|
||||||
|
taskId: pairedExecutionContext?.task.id,
|
||||||
|
role: completedRole,
|
||||||
|
pairedExecutionStatus: state.effectiveStatus,
|
||||||
|
taskStatus: finishedTask.status,
|
||||||
|
intentKind: followUpResult.intentKind,
|
||||||
|
scheduled: followUpResult.scheduled,
|
||||||
|
},
|
||||||
|
followUpResult.scheduled
|
||||||
|
? 'Queued paired follow-up after failed reviewer/arbiter execution left a pending task state'
|
||||||
|
: 'Skipped duplicate paired follow-up after failed reviewer/arbiter execution while task state was unchanged',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPairedExecutionLifecycle(
|
||||||
|
args: CreatePairedExecutionLifecycleArgs,
|
||||||
|
): PairedExecutionLifecycle {
|
||||||
|
return new PairedExecutionLifecycleController(args);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user