Carry forward the arbiter verdict too, not just the owner final

When a paired task ends on an arbiter ESCALATE (asking the user to choose), the
user's next reply was carried into the fresh task with the owner's earlier final
as context — not the arbiter message they were actually responding to. Generalize
the carry-forward to the latest user-facing final (owner final OR arbiter
verdict, whichever the task ended on), so the user can choose based on the last
arbiter message without the new turn re-reading the whole prior conversation.
The carried-forward guidance marker is now matched by a shared prefix so it
triggers for both. Adds tests for the arbiter-escalation carry and the prefix
guidance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-08-28 20:44:22 +09:00
parent 5376df5389
commit 2259471b66
4 changed files with 123 additions and 22 deletions

View File

@@ -251,20 +251,28 @@ function isIntermediateStepOutput(outputText: string): boolean {
return /^\s*STEP_DONE\b/.test(outputText);
}
function getLatestOwnerFinalOutput(taskId: string): PairedTurnOutput | null {
const ownerOutputs = [...getPairedTurnOutputs(taskId)]
function getLatestUserFacingFinalOutput(
taskId: string,
): PairedTurnOutput | null {
// The user only ever sees owner finals and arbiter verdicts (reviewer turns
// are internal). Whichever of those the previous task ended on is what the
// user is replying to next — e.g. an arbiter ESCALATE asking them to choose.
// So carry the chronologically-latest owner/arbiter final forward.
const userFacingOutputs = [...getPairedTurnOutputs(taskId)]
.reverse()
.filter((output) => output.role === 'owner');
.filter(
(output) => output.role === 'owner' || output.role === 'arbiter',
);
return (
ownerOutputs.find(
userFacingOutputs.find(
(output) => !isIntermediateStepOutput(output.output_text),
) ??
ownerOutputs[0] ??
userFacingOutputs[0] ??
null
);
}
function carryForwardLatestOwnerFinal(args: {
function carryForwardLatestFinal(args: {
sourceTask: PairedTask;
targetTask: PairedTask;
}): void {
@@ -272,29 +280,34 @@ function carryForwardLatestOwnerFinal(args: {
return;
}
const latestOwnerFinal = getLatestOwnerFinalOutput(args.sourceTask.id);
if (!latestOwnerFinal) {
const latestFinal = getLatestUserFacingFinalOutput(args.sourceTask.id);
if (!latestFinal) {
return;
}
const label =
latestFinal.role === 'arbiter'
? 'latest arbiter verdict'
: 'latest owner final';
insertPairedTurnOutput(
args.targetTask.id,
0,
'owner',
`[Carried forward context from the previous task: latest owner final]\n${latestOwnerFinal.output_text}`,
`[Carried forward context from the previous task: ${label}]\n${latestFinal.output_text}`,
{
createdAt: latestOwnerFinal.created_at,
attachments: latestOwnerFinal.attachments,
createdAt: latestFinal.created_at,
attachments: latestFinal.attachments,
},
);
logger.info(
{
sourceTaskId: args.sourceTask.id,
targetTaskId: args.targetTask.id,
carriedChars: latestOwnerFinal.output_text.length,
attachmentCount: latestOwnerFinal.attachments?.length ?? 0,
carriedRole: latestFinal.role,
carriedChars: latestFinal.output_text.length,
attachmentCount: latestFinal.attachments?.length ?? 0,
},
'Carried forward latest owner final into superseding paired task',
'Carried forward latest user-facing final into superseding paired task',
);
}
@@ -328,7 +341,7 @@ export function resolveOwnerTaskForHumanMessage(args: {
// fresh task with the previous task's latest owner final so the owner keeps
// continuity across sessions instead of answering with "no context".
if (newTask && previousTask) {
carryForwardLatestOwnerFinal({
carryForwardLatestFinal({
sourceTask: previousTask,
targetTask: newTask,
});
@@ -373,7 +386,7 @@ export function resolveOwnerTaskForHumanMessage(args: {
canonicalWorkDir,
roomRoleContext: args.roomRoleContext,
});
carryForwardLatestOwnerFinal({
carryForwardLatestFinal({
sourceTask: existing,
targetTask: newTask,
});