fix dashboard progress turn activity

This commit is contained in:
ejclaw
2026-04-29 20:04:47 +09:00
parent 4aca45ca23
commit b653fe5375
4 changed files with 153 additions and 4 deletions

View File

@@ -0,0 +1,62 @@
import { beforeEach, describe, expect, it } from 'vitest';
import {
_initTestDatabase,
getLatestPairedTurnForTask,
markPairedTurnRunning,
reservePairedTurnReservation,
updatePairedTurnProgressText,
} from './db.js';
import { CODEX_MAIN_SERVICE_ID } from './config.js';
import { buildPairedTurnIdentity } from './paired-turn-identity.js';
beforeEach(() => {
_initTestDatabase();
});
describe('paired turn progress activity', () => {
it('selects the latest paired turn by progress activity timestamp', async () => {
const taskId = 'progress-latest-task';
const activeTurn = buildPairedTurnIdentity({
taskId,
taskUpdatedAt: '2026-04-10T00:00:00.000Z',
intentKind: 'owner-follow-up',
role: 'owner',
});
const queuedTurn = buildPairedTurnIdentity({
taskId,
taskUpdatedAt: '2026-04-10T00:01:00.000Z',
intentKind: 'owner-follow-up',
role: 'owner',
});
markPairedTurnRunning({
turnIdentity: activeTurn,
executorServiceId: CODEX_MAIN_SERVICE_ID,
executorAgentType: 'codex',
runId: 'run-progress-active',
});
expect(
reservePairedTurnReservation({
chatJid: 'dc:ops',
taskId,
taskStatus: 'active',
roundTripCount: 1,
taskUpdatedAt: queuedTurn.taskUpdatedAt,
intentKind: queuedTurn.intentKind,
runId: 'run-queued-empty',
}),
).toBe(true);
await new Promise((resolve) => setTimeout(resolve, 5));
updatePairedTurnProgressText(
activeTurn.turnId,
'checking dashboard parity',
);
expect(getLatestPairedTurnForTask(taskId)).toMatchObject({
turn_id: activeTurn.turnId,
progress_text: 'checking dashboard parity',
});
});
});

View File

@@ -507,16 +507,19 @@ export function updatePairedTurnProgressTextFromDatabase(
turnId: string,
progressText: string | null,
): void {
const now = new Date().toISOString();
let stmt = updateProgressTextStmtCache.get(database);
if (!stmt) {
stmt = database.prepare(`
UPDATE paired_turns
SET progress_text = ?, progress_updated_at = ?
SET progress_text = ?,
progress_updated_at = ?,
updated_at = ?
WHERE turn_id = ?
`);
updateProgressTextStmtCache.set(database, stmt);
}
stmt.run(progressText, new Date().toISOString(), turnId);
stmt.run(progressText, now, now, turnId);
}
const latestPairedTurnStmtCache = new WeakMap<
@@ -534,7 +537,15 @@ export function getLatestPairedTurnForTaskFromDatabase(
SELECT *
FROM paired_turns
WHERE task_id = ?
ORDER BY updated_at DESC, turn_id DESC
ORDER BY CASE
WHEN progress_text IS NOT NULL
AND trim(progress_text) <> ''
AND progress_updated_at IS NOT NULL
AND progress_updated_at > updated_at
THEN progress_updated_at
ELSE updated_at
END DESC,
turn_id DESC
LIMIT 1
`);
latestPairedTurnStmtCache.set(database, stmt);

View File

@@ -646,6 +646,69 @@ describe('web dashboard room activity data', () => {
]);
});
it('keeps a progress-updated turn current when a newer empty reservation exists', () => {
const task = makePairedTask({
id: 'paired-room-progress-race',
chat_jid: 'dc:ops',
status: 'active',
updated_at: '2026-04-26T06:10:00.000Z',
});
const activeProgressTurn: PairedTurnRecord = {
turn_id: 'turn-progress-active',
task_id: task.id,
task_updated_at: task.updated_at,
role: 'owner',
intent_kind: 'owner-follow-up',
state: 'running',
executor_service_id: 'codex-main',
executor_agent_type: 'codex',
attempt_no: 1,
created_at: '2026-04-26T06:00:00.000Z',
updated_at: '2026-04-26T06:10:00.000Z',
completed_at: null,
last_error: null,
progress_text: 'checking dashboard parity',
progress_updated_at: '2026-04-26T06:12:00.000Z',
};
const queuedEmptyTurn: PairedTurnRecord = {
...activeProgressTurn,
turn_id: 'turn-queued-empty',
state: 'queued',
executor_service_id: null,
executor_agent_type: null,
attempt_no: 0,
created_at: '2026-04-26T06:11:00.000Z',
updated_at: '2026-04-26T06:11:00.000Z',
progress_text: null,
progress_updated_at: null,
};
const activity = buildWebDashboardRoomActivity({
serviceId: 'codex-main',
entry: {
jid: 'dc:ops',
name: '#ops',
folder: 'ops',
agentType: 'codex',
status: 'processing',
elapsedMs: 20_000,
pendingMessages: false,
pendingTasks: 0,
},
pairedTask: task,
turns: [activeProgressTurn, queuedEmptyTurn],
attempts: [],
outputs: [],
messages: [],
});
expect(activity.pairedTask?.currentTurn).toMatchObject({
turnId: 'turn-progress-active',
progressText: 'checking dashboard parity',
progressUpdatedAt: '2026-04-26T06:12:00.000Z',
});
});
it('does not show active turn placeholders when canonical progress is absent', () => {
const task = makePairedTask({
id: 'paired-progress-internal',

View File

@@ -594,6 +594,17 @@ function compareRoomMessagesByTimestamp(
return aTime - bTime;
}
function getRoomTurnActivityTimestamp(turn: PairedTurnRecord): string {
const progressUpdatedAt =
turn.completed_at === null && turn.progress_text?.trim()
? turn.progress_updated_at
: null;
if (progressUpdatedAt && progressUpdatedAt > turn.updated_at) {
return progressUpdatedAt;
}
return turn.updated_at;
}
const TASK_STATUS_PREFIX = '';
function isTaskStatusMessage(message: NewMessage): boolean {
@@ -673,7 +684,9 @@ export function buildWebDashboardRoomActivity(args: {
}
const currentTurn =
[...args.turns].sort((a, b) =>
b.updated_at.localeCompare(a.updated_at),
getRoomTurnActivityTimestamp(b).localeCompare(
getRoomTurnActivityTimestamp(a),
),
)[0] ?? null;
const currentAttempt = currentTurn
? (latestAttemptByTurnId.get(currentTurn.turn_id) ?? null)