fix: preserve reviewer delivery follow-ups
This commit is contained in:
@@ -21,11 +21,50 @@ export type PairedFollowUpSource = Parameters<
|
|||||||
typeof resolveFollowUpDispatch
|
typeof resolveFollowUpDispatch
|
||||||
>[0]['source'];
|
>[0]['source'];
|
||||||
|
|
||||||
|
type PairedTurnOutputContext = ReturnType<typeof getPairedTurnOutputs>[number];
|
||||||
|
|
||||||
type PairedFollowUpTaskContext = Pick<
|
type PairedFollowUpTaskContext = Pick<
|
||||||
PairedTask,
|
PairedTask,
|
||||||
'id' | 'status' | 'round_trip_count' | 'updated_at' | 'owner_failure_count'
|
'id' | 'status' | 'round_trip_count' | 'updated_at' | 'owner_failure_count'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
function timestampMs(value: string | null | undefined): number | null {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parsed = Date.parse(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPersistedOutputOlderThanTaskRevision(args: {
|
||||||
|
output: PairedTurnOutputContext;
|
||||||
|
task: Partial<Pick<PairedTask, 'updated_at'>> | null | undefined;
|
||||||
|
}): boolean {
|
||||||
|
const outputCreatedAt = timestampMs(args.output.created_at);
|
||||||
|
const taskUpdatedAt = timestampMs(args.task?.updated_at);
|
||||||
|
if (outputCreatedAt == null || taskUpdatedAt == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return outputCreatedAt < taskUpdatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPreferFallbackTurnOutputContext(args: {
|
||||||
|
output: PairedTurnOutputContext | null | undefined;
|
||||||
|
task: Partial<Pick<PairedTask, 'updated_at'>> | null | undefined;
|
||||||
|
fallbackRole: PairedRoomRole | null | undefined;
|
||||||
|
}): boolean {
|
||||||
|
if (!args.output || !args.fallbackRole) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (args.output.role === args.fallbackRole) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isPersistedOutputOlderThanTaskRevision({
|
||||||
|
output: args.output,
|
||||||
|
task: args.task,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export interface PairedFollowUpDecision {
|
export interface PairedFollowUpDecision {
|
||||||
taskId: string | null;
|
taskId: string | null;
|
||||||
taskStatus: PairedTaskStatus | null;
|
taskStatus: PairedTaskStatus | null;
|
||||||
@@ -49,7 +88,10 @@ export type PairedFollowUpDispatchResult =
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function resolveLatestPairedTurnOutputContext(args: {
|
export function resolveLatestPairedTurnOutputContext(args: {
|
||||||
task: Pick<PairedTask, 'id'> | null | undefined;
|
task:
|
||||||
|
| (Pick<PairedTask, 'id'> & Partial<Pick<PairedTask, 'updated_at'>>)
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
fallbackLastTurnOutputRole?: PairedRoomRole | null;
|
fallbackLastTurnOutputRole?: PairedRoomRole | null;
|
||||||
fallbackLastTurnOutputVerdict?: VisibleVerdict | null;
|
fallbackLastTurnOutputVerdict?: VisibleVerdict | null;
|
||||||
}): {
|
}): {
|
||||||
@@ -59,12 +101,19 @@ export function resolveLatestPairedTurnOutputContext(args: {
|
|||||||
const latestOutput = args.task
|
const latestOutput = args.task
|
||||||
? getPairedTurnOutputs(args.task.id).at(-1)
|
? getPairedTurnOutputs(args.task.id).at(-1)
|
||||||
: null;
|
: null;
|
||||||
|
const output = shouldPreferFallbackTurnOutputContext({
|
||||||
|
output: latestOutput,
|
||||||
|
task: args.task,
|
||||||
|
fallbackRole: args.fallbackLastTurnOutputRole,
|
||||||
|
})
|
||||||
|
? null
|
||||||
|
: latestOutput;
|
||||||
return {
|
return {
|
||||||
role: latestOutput?.role ?? args.fallbackLastTurnOutputRole ?? null,
|
role: output?.role ?? args.fallbackLastTurnOutputRole ?? null,
|
||||||
verdict:
|
verdict:
|
||||||
resolveStoredVisibleVerdict({
|
resolveStoredVisibleVerdict({
|
||||||
verdict: latestOutput?.verdict ?? null,
|
verdict: output?.verdict ?? null,
|
||||||
outputText: latestOutput?.output_text ?? null,
|
outputText: output?.output_text ?? null,
|
||||||
}) ??
|
}) ??
|
||||||
args.fallbackLastTurnOutputVerdict ??
|
args.fallbackLastTurnOutputVerdict ??
|
||||||
null,
|
null,
|
||||||
@@ -73,7 +122,10 @@ export function resolveLatestPairedTurnOutputContext(args: {
|
|||||||
|
|
||||||
export function resolvePairedFollowUpDecision(args: {
|
export function resolvePairedFollowUpDecision(args: {
|
||||||
task:
|
task:
|
||||||
| Pick<PairedFollowUpTaskContext, 'id' | 'status' | 'owner_failure_count'>
|
| Pick<
|
||||||
|
PairedFollowUpTaskContext,
|
||||||
|
'id' | 'status' | 'updated_at' | 'owner_failure_count'
|
||||||
|
>
|
||||||
| null
|
| null
|
||||||
| undefined;
|
| undefined;
|
||||||
source: PairedFollowUpSource;
|
source: PairedFollowUpSource;
|
||||||
|
|||||||
@@ -2727,9 +2727,20 @@ describe('createMessageRuntime', () => {
|
|||||||
vi.mocked(db.getLatestOpenPairedTaskForChat).mockImplementation(
|
vi.mocked(db.getLatestOpenPairedTaskForChat).mockImplementation(
|
||||||
() => pairedTask,
|
() => pairedTask,
|
||||||
);
|
);
|
||||||
|
vi.mocked(db.getPairedTurnOutputs).mockReturnValue([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
task_id: pairedTask.id,
|
||||||
|
turn_number: 1,
|
||||||
|
role: 'owner',
|
||||||
|
output_text: 'STEP_DONE\nowner work ready for review',
|
||||||
|
created_at: '2026-03-30T00:00:00.500Z',
|
||||||
|
},
|
||||||
|
] as any);
|
||||||
vi.mocked(agentRunner.runAgentProcess).mockImplementation(
|
vi.mocked(agentRunner.runAgentProcess).mockImplementation(
|
||||||
async (_group, _input, _onProcess, onOutput) => {
|
async (_group, _input, _onProcess, onOutput) => {
|
||||||
pairedTask.status = 'active';
|
pairedTask.status = 'active';
|
||||||
|
pairedTask.updated_at = '2026-03-30T00:00:01.000Z';
|
||||||
await onOutput?.({
|
await onOutput?.({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
phase: 'final',
|
phase: 'final',
|
||||||
|
|||||||
Reference in New Issue
Block a user