Remove previous owner final prompt carryover

This commit is contained in:
ejclaw
2026-05-16 19:14:25 +09:00
parent 04838a1e9f
commit e06c5af70f
5 changed files with 31 additions and 164 deletions

View File

@@ -1,6 +1,5 @@
import { import {
getLastHumanMessageContent, getLastHumanMessageContent,
getLatestPreviousPairedTaskForChat,
getPairedTurnOutputs, getPairedTurnOutputs,
getRecentChatMessages, getRecentChatMessages,
} from './db.js'; } from './db.js';
@@ -11,7 +10,6 @@ import {
buildOwnerPendingPrompt, buildOwnerPendingPrompt,
buildPairedTurnPrompt, buildPairedTurnPrompt,
buildReviewerPendingPrompt, buildReviewerPendingPrompt,
type PriorTaskPromptContext,
} from './message-runtime-prompts.js'; } from './message-runtime-prompts.js';
import { import {
requeuePendingPairedTurn, requeuePendingPairedTurn,
@@ -35,7 +33,6 @@ import type {
NewMessage, NewMessage,
PairedTask, PairedTask,
PairedRoomRole, PairedRoomRole,
PairedTurnOutput,
PairedTurnReservationIntentKind, PairedTurnReservationIntentKind,
RegisteredGroup, RegisteredGroup,
} from './types.js'; } from './types.js';
@@ -119,11 +116,6 @@ export function buildPendingPairedTurn(args: {
const cursor = lastRaw?.seq ?? lastRaw?.timestamp ?? null; const cursor = lastRaw?.seq ?? lastRaw?.timestamp ?? null;
const taskStatus = task.status; const taskStatus = task.status;
const turnOutputs = getPairedTurnOutputs(task.id); const turnOutputs = getPairedTurnOutputs(task.id);
const priorTaskContext = resolvePriorTaskPromptContext(
chatJid,
task,
turnOutputs,
);
const lastTurnOutput = turnOutputs[turnOutputs.length - 1]; const lastTurnOutput = turnOutputs[turnOutputs.length - 1];
const nextTurnAction = resolveNextTurnAction({ const nextTurnAction = resolveNextTurnAction({
taskStatus, taskStatus,
@@ -144,7 +136,6 @@ export function buildPendingPairedTurn(args: {
turnOutputs, turnOutputs,
recentHumanMessages, recentHumanMessages,
lastHumanMessage, lastHumanMessage,
priorTaskContext,
}), }),
channel: resolveChannel(taskStatus), channel: resolveChannel(taskStatus),
cursor, cursor,
@@ -196,7 +187,6 @@ export function buildPendingPairedTurn(args: {
turnOutputs, turnOutputs,
recentHumanMessages, recentHumanMessages,
lastHumanMessage, lastHumanMessage,
priorTaskContext,
}), }),
channel: resolveChannel(taskStatus), channel: resolveChannel(taskStatus),
cursor, cursor,
@@ -210,38 +200,6 @@ export function buildPendingPairedTurn(args: {
return null; return null;
} }
function resolvePriorTaskPromptContext(
chatJid: string,
task: PairedTask,
turnOutputs: PairedTurnOutput[],
): PriorTaskPromptContext | null {
if (turnOutputs.length > 0) {
return null;
}
const previousTask = getLatestPreviousPairedTaskForChat(chatJid, task.id);
if (!previousTask || previousTask.status !== 'completed') {
return null;
}
const previousOutputs = getPairedTurnOutputs(previousTask.id);
const ownerFinal = [...previousOutputs]
.reverse()
.find((output) => output.role === 'owner')?.output_text;
const reviewerFinal = [...previousOutputs]
.reverse()
.find((output) => output.role === 'reviewer')?.output_text;
if (!ownerFinal && !reviewerFinal) {
return null;
}
return {
ownerFinal: ownerFinal ?? null,
reviewerFinal: reviewerFinal ?? null,
};
}
export async function executePendingPairedTurn(args: { export async function executePendingPairedTurn(args: {
pendingTurn: Exclude<PendingPairedTurn, null>; pendingTurn: Exclude<PendingPairedTurn, null>;
chatJid: string; chatJid: string;

View File

@@ -69,7 +69,6 @@ describe('message-runtime-prompts carry-forward guidance', () => {
], ],
recentHumanMessages: [makeHumanMessage('이제 새 질문')], recentHumanMessages: [makeHumanMessage('이제 새 질문')],
lastHumanMessage: '이제 새 질문', lastHumanMessage: '이제 새 질문',
priorTaskContext: null,
}); });
expect( expect(
@@ -89,7 +88,6 @@ describe('message-runtime-prompts carry-forward guidance', () => {
], ],
recentHumanMessages: [makeHumanMessage('새 owner 질문')], recentHumanMessages: [makeHumanMessage('새 owner 질문')],
lastHumanMessage: '새 owner 질문', lastHumanMessage: '새 owner 질문',
priorTaskContext: null,
}); });
expect( expect(
@@ -115,72 +113,54 @@ describe('message-runtime-prompts carry-forward guidance', () => {
).toBe(false); ).toBe(false);
}); });
it('includes previous owner finals in reviewer pending prompts when the current task has no outputs', () => { it('does not include previous task context in reviewer pending prompts when the current task has no outputs', () => {
const prompt = buildReviewerPendingPrompt({ const prompt = buildReviewerPendingPrompt({
chatJid: 'group@test', chatJid: 'group@test',
timezone: 'UTC', timezone: 'UTC',
turnOutputs: [], turnOutputs: [],
recentHumanMessages: [makeHumanMessage('추가 질문')], recentHumanMessages: [makeHumanMessage('추가 질문')],
lastHumanMessage: '추가 질문', lastHumanMessage: '추가 질문',
priorTaskContext: {
ownerFinal: 'DONE\n이전 owner 결론',
reviewerFinal: 'DONE_WITH_CONCERNS\n이전 reviewer 결론',
},
}); });
expect(prompt).toContain( expect(prompt).toContain('추가 질문');
expect(prompt).not.toContain(
'Background from the previous completed paired task:', 'Background from the previous completed paired task:',
); );
expect(prompt).toContain('Previous task owner final:'); expect(prompt).not.toContain('Previous task owner final:');
expect(prompt).toContain('이전 owner 결론'); expect(prompt).not.toContain('이전 owner 결론');
expect(prompt).not.toContain('Previous task reviewer final:'); expect(prompt).not.toContain('Previous task reviewer final:');
expect(prompt).not.toContain('이전 reviewer 결론'); expect(prompt).not.toContain('이전 reviewer 결론');
}); });
it('includes previous owner finals in owner pending prompts when the current task has no outputs', () => { it('does not include previous task context in owner pending prompts when the current task has no outputs', () => {
const prompt = buildOwnerPendingPrompt({ const prompt = buildOwnerPendingPrompt({
chatJid: 'group@test', chatJid: 'group@test',
timezone: 'UTC', timezone: 'UTC',
turnOutputs: [], turnOutputs: [],
recentHumanMessages: [makeHumanMessage('추가 owner 질문')], recentHumanMessages: [makeHumanMessage('추가 owner 질문')],
lastHumanMessage: '추가 owner 질문', lastHumanMessage: '추가 owner 질문',
priorTaskContext: {
ownerFinal: 'DONE\n이전 owner 결론',
reviewerFinal: 'DONE_WITH_CONCERNS\n이전 reviewer 결론',
},
}); });
expect(prompt).toContain( expect(prompt).toContain('추가 owner 질문');
expect(prompt).not.toContain(
'Background from the previous completed paired task:', 'Background from the previous completed paired task:',
); );
expect(prompt).toContain('Previous task owner final:'); expect(prompt).not.toContain('Previous task owner final:');
expect(prompt).not.toContain('이전 owner 결론');
expect(prompt).not.toContain('Previous task reviewer final:'); expect(prompt).not.toContain('Previous task reviewer final:');
expect(prompt).toContain('추가 owner 질문'); expect(prompt).not.toContain('이전 reviewer 결론');
}); });
it('omits previous reviewer finals from prior task context', () => { it('does not reinject previous reviewer output into new reviewer prompts', () => {
const prompt = buildReviewerPendingPrompt({ const prompt = buildReviewerPendingPrompt({
chatJid: 'group@test', chatJid: 'group@test',
timezone: 'UTC', timezone: 'UTC',
turnOutputs: [], turnOutputs: [],
recentHumanMessages: [makeHumanMessage('새 작업')], recentHumanMessages: [makeHumanMessage('새 작업')],
lastHumanMessage: '새 작업', lastHumanMessage: '새 작업',
priorTaskContext: {
ownerFinal: null,
reviewerFinal: `TASK_DONE
검증한 것:
- CI pass
남은 항목 (변동 없음):
(관찰) 사용자 직접 functional 검증
(잠재) 다른 enum 영어 라벨 추가 발견 시
현재 시리즈 상태:
#129 Settings IA
#130 Runtime Inventory`,
},
}); });
expect(prompt).toContain('새 작업');
expect(prompt).not.toContain('Previous task reviewer final:'); expect(prompt).not.toContain('Previous task reviewer final:');
expect(prompt).not.toContain('검증한 것:'); expect(prompt).not.toContain('검증한 것:');
expect(prompt).not.toContain('CI pass'); expect(prompt).not.toContain('CI pass');

View File

@@ -8,11 +8,6 @@ const CARRIED_FORWARD_OWNER_FINAL_MARKER =
const CARRIED_FORWARD_OWNER_FINAL_GUIDANCE = `System note: const CARRIED_FORWARD_OWNER_FINAL_GUIDANCE = `System note:
If you see a message beginning with "${CARRIED_FORWARD_OWNER_FINAL_MARKER}", treat it as background only. Do not repeat, continue, or answer that carried-forward final directly. Respond only to the latest human request and the current task.`; If you see a message beginning with "${CARRIED_FORWARD_OWNER_FINAL_MARKER}", treat it as background only. Do not repeat, continue, or answer that carried-forward final directly. Respond only to the latest human request and the current task.`;
export interface PriorTaskPromptContext {
ownerFinal?: string | null;
reviewerFinal?: string | null;
}
function turnOutputsToMessages( function turnOutputsToMessages(
outputs: PairedTurnOutput[], outputs: PairedTurnOutput[],
chatJid: string, chatJid: string,
@@ -56,35 +51,6 @@ function prependCarriedForwardGuidance(
return `${CARRIED_FORWARD_OWNER_FINAL_GUIDANCE}\n\n${prompt}`; return `${CARRIED_FORWARD_OWNER_FINAL_GUIDANCE}\n\n${prompt}`;
} }
function truncatePriorTaskFinal(text: string, maxChars = 1200): string {
const normalized = text.trim();
if (normalized.length <= maxChars) {
return normalized;
}
return `${normalized.slice(0, maxChars)}...`;
}
function formatPriorTaskPromptContext(
priorTaskContext?: PriorTaskPromptContext | null,
): string {
if (!priorTaskContext) {
return '';
}
const sections: string[] = [];
if (priorTaskContext.ownerFinal?.trim()) {
sections.push(
`Previous task owner final:\n---\n${truncatePriorTaskFinal(priorTaskContext.ownerFinal)}\n---`,
);
}
if (sections.length === 0) {
return '';
}
return `Background from the previous completed paired task:\n${sections.join('\n\n')}\n\n`;
}
export function buildPairedTurnPrompt(args: { export function buildPairedTurnPrompt(args: {
taskId: string; taskId: string;
chatJid: string; chatJid: string;
@@ -119,7 +85,6 @@ export function buildReviewerPendingPrompt(args: {
turnOutputs: PairedTurnOutput[]; turnOutputs: PairedTurnOutput[];
recentHumanMessages: NewMessage[]; recentHumanMessages: NewMessage[];
lastHumanMessage: string | null | undefined; lastHumanMessage: string | null | undefined;
priorTaskContext?: PriorTaskPromptContext | null;
}): string { }): string {
if (args.turnOutputs.length > 0) { if (args.turnOutputs.length > 0) {
return prependCarriedForwardGuidance( return prependCarriedForwardGuidance(
@@ -136,10 +101,10 @@ export function buildReviewerPendingPrompt(args: {
} }
if (!args.lastHumanMessage) { if (!args.lastHumanMessage) {
return `${formatPriorTaskPromptContext(args.priorTaskContext)}Review the latest owner changes in the workspace.`; return 'Review the latest owner changes in the workspace.';
} }
return `${formatPriorTaskPromptContext(args.priorTaskContext)}User request:\n---\n${args.lastHumanMessage}\n---\n\nReview the latest owner changes in the workspace.`; return `User request:\n---\n${args.lastHumanMessage}\n---\n\nReview the latest owner changes in the workspace.`;
} }
export function buildOwnerPendingPrompt(args: { export function buildOwnerPendingPrompt(args: {
@@ -148,7 +113,6 @@ export function buildOwnerPendingPrompt(args: {
turnOutputs: PairedTurnOutput[]; turnOutputs: PairedTurnOutput[];
recentHumanMessages: NewMessage[]; recentHumanMessages: NewMessage[];
lastHumanMessage: string | null | undefined; lastHumanMessage: string | null | undefined;
priorTaskContext?: PriorTaskPromptContext | null;
}): string { }): string {
if (args.turnOutputs.length > 0) { if (args.turnOutputs.length > 0) {
return prependCarriedForwardGuidance( return prependCarriedForwardGuidance(
@@ -165,10 +129,10 @@ export function buildOwnerPendingPrompt(args: {
} }
if (!args.lastHumanMessage) { if (!args.lastHumanMessage) {
return `${formatPriorTaskPromptContext(args.priorTaskContext)}Continue the owner turn using the latest reviewer or arbiter feedback.`; return 'Continue the owner turn using the latest reviewer or arbiter feedback.';
} }
return `${formatPriorTaskPromptContext(args.priorTaskContext)}User request:\n---\n${args.lastHumanMessage}\n---\n\nContinue the owner turn using the latest reviewer or arbiter feedback.`; return `User request:\n---\n${args.lastHumanMessage}\n---\n\nContinue the owner turn using the latest reviewer or arbiter feedback.`;
} }
export function buildArbiterPromptForTask(args: { export function buildArbiterPromptForTask(args: {

View File

@@ -1994,7 +1994,7 @@ describe('createMessageRuntime', () => {
}); });
}); });
it('includes previous task owner final but omits reviewer final in a new reviewer prompt when the current task has no outputs', () => { it('does not reinject previous task finals in a new reviewer prompt when the current task has no outputs', () => {
const chatJid = 'group@test'; const chatJid = 'group@test';
const group = makeGroup('claude-code'); const group = makeGroup('claude-code');
const currentTask = { const currentTask = {
@@ -2015,44 +2015,7 @@ describe('createMessageRuntime', () => {
created_at: '2026-03-30T00:00:10.000Z', created_at: '2026-03-30T00:00:10.000Z',
updated_at: '2026-03-30T00:00:10.000Z', updated_at: '2026-03-30T00:00:10.000Z',
} as any; } as any;
const previousTask = { vi.mocked(db.getPairedTurnOutputs).mockReturnValue([]);
...currentTask,
id: 'task-previous-reviewer',
status: 'completed',
completion_reason: 'superseded',
created_at: '2026-03-30T00:00:00.000Z',
updated_at: '2026-03-30T00:00:09.000Z',
} as any;
vi.mocked(db.getLatestPreviousPairedTaskForChat).mockReturnValue(
previousTask,
);
vi.mocked(db.getPairedTurnOutputs).mockImplementation((taskId: string) => {
if (taskId === currentTask.id) {
return [];
}
if (taskId === previousTask.id) {
return [
{
id: 1,
task_id: previousTask.id,
turn_number: 1,
role: 'owner',
output_text: 'DONE\n이전 owner 답변',
created_at: '2026-03-30T00:00:05.000Z',
},
{
id: 2,
task_id: previousTask.id,
turn_number: 2,
role: 'reviewer',
output_text: 'DONE_WITH_CONCERNS\n이전 reviewer 피드백',
created_at: '2026-03-30T00:00:06.000Z',
},
] as any;
}
return [];
});
vi.mocked(db.getLastHumanMessageContent).mockReturnValue('추가 질문'); vi.mocked(db.getLastHumanMessageContent).mockReturnValue('추가 질문');
const pending = buildPendingPairedTurn({ const pending = buildPendingPairedTurn({
@@ -2066,11 +2029,13 @@ describe('createMessageRuntime', () => {
}); });
expect(pending).not.toBeNull(); expect(pending).not.toBeNull();
expect(pending?.prompt).toContain( expect(db.getLatestPreviousPairedTaskForChat).not.toHaveBeenCalled();
expect(pending?.prompt).toContain('추가 질문');
expect(pending?.prompt).not.toContain(
'Background from the previous completed paired task:', 'Background from the previous completed paired task:',
); );
expect(pending?.prompt).toContain('Previous task owner final:'); expect(pending?.prompt).not.toContain('Previous task owner final:');
expect(pending?.prompt).toContain('이전 owner 답변'); expect(pending?.prompt).not.toContain('이전 owner 답변');
expect(pending?.prompt).not.toContain('Previous task reviewer final:'); expect(pending?.prompt).not.toContain('Previous task reviewer final:');
expect(pending?.prompt).not.toContain('이전 reviewer 피드백'); expect(pending?.prompt).not.toContain('이전 reviewer 피드백');
}); });

View File

@@ -11,22 +11,22 @@ const ONE_PIXEL_PNG = Buffer.from(
'base64', 'base64',
); );
const tempFiles: string[] = []; const tempDirs: string[] = [];
afterEach(() => { afterEach(() => {
for (const file of tempFiles.splice(0)) { for (const dir of tempDirs.splice(0)) {
fs.rmSync(file, { force: true }); fs.rmSync(dir, { force: true, recursive: true });
} }
}); });
describe('web dashboard attachment previews', () => { describe('web dashboard attachment previews', () => {
it('serves validated direct temp image attachments', async () => { it('serves validated direct temp image attachments', async () => {
const filePath = path.join( const tempDir = fs.mkdtempSync(
os.tmpdir(), path.join(os.tmpdir(), 'ejclaw-dashboard-attachment-'),
`bar-chart-label-fit-playwright-${Date.now()}.png`,
); );
tempDirs.push(tempDir);
const filePath = path.join(tempDir, 'bar-chart-label-fit-playwright.png');
fs.writeFileSync(filePath, ONE_PIXEL_PNG); fs.writeFileSync(filePath, ONE_PIXEL_PNG);
tempFiles.push(filePath);
const handler = createWebDashboardHandler({ const handler = createWebDashboardHandler({
readStatusSnapshots: () => [], readStatusSnapshots: () => [],
getTasks: () => [], getTasks: () => [],