fix dashboard progress turn activity
This commit is contained in:
62
src/db-paired-turn-progress.test.ts
Normal file
62
src/db-paired-turn-progress.test.ts
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user