fix(status): keep task snapshots pure

This commit is contained in:
Vincent Koc
2026-04-01 16:35:30 +09:00
parent 5a95d65f1e
commit cfa307baed
6 changed files with 178 additions and 17 deletions

View File

@@ -0,0 +1,76 @@
import { describe, expect, it } from "vitest";
import type { TaskRecord } from "./task-registry.types.js";
import {
buildTaskStatusSnapshot,
formatTaskStatusDetail,
formatTaskStatusTitle,
} from "./task-status.js";
const NOW = 1_000_000_000_000;
function makeTask(overrides: Partial<TaskRecord>): TaskRecord {
return {
taskId: "task-1",
runId: "run-1",
task: "default task",
runtime: "subagent",
status: "running",
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
createdAt: NOW - 1_000,
deliveryStatus: "pending",
notifyPolicy: "done_only",
...overrides,
};
}
describe("task status snapshot", () => {
it("keeps old active tasks active without maintenance reconciliation", () => {
const staleButActive = makeTask({
createdAt: NOW - 10 * 60_000,
startedAt: NOW - 10 * 60_000,
lastEventAt: NOW - 10 * 60_000,
progressSummary: "still running",
});
const snapshot = buildTaskStatusSnapshot([staleButActive], { now: NOW });
expect(snapshot.activeCount).toBe(1);
expect(snapshot.recentFailureCount).toBe(0);
expect(snapshot.focus?.status).toBe("running");
expect(snapshot.focus?.taskId).toBe("task-1");
});
it("filters tasks whose cleanupAfter has expired", () => {
const expired = makeTask({
status: "succeeded",
endedAt: NOW - 60_000,
cleanupAfter: NOW - 1,
});
const snapshot = buildTaskStatusSnapshot([expired], { now: NOW });
expect(snapshot.totalCount).toBe(0);
expect(snapshot.focus).toBeUndefined();
});
});
describe("task status formatting", () => {
it("truncates long task titles and details", () => {
const task = makeTask({
task: "This is a deliberately long task prompt that should never be emitted in full because it may include internal instructions and file paths.",
progressSummary:
"This progress detail is also intentionally long so the status line proves it truncates verbose task context instead of dumping a wall of text.",
});
expect(formatTaskStatusTitle(task)).toContain(
"This is a deliberately long task prompt that should never be emitted in full",
);
expect(formatTaskStatusTitle(task).endsWith("…")).toBe(true);
expect(formatTaskStatusDetail(task)).toContain(
"This progress detail is also intentionally long so the status line proves it truncates verbose task context",
);
expect(formatTaskStatusDetail(task)?.endsWith("…")).toBe(true);
});
});

View File

@@ -1,9 +1,11 @@
import { reconcileTaskRecordForOperatorInspection } from "./task-registry.maintenance.js";
import { truncateUtf16Safe } from "../utils.js";
import type { TaskRecord } from "./task-registry.types.js";
const ACTIVE_TASK_STATUSES = new Set(["queued", "running"]);
const FAILURE_TASK_STATUSES = new Set(["failed", "timed_out", "lost"]);
export const TASK_STATUS_RECENT_WINDOW_MS = 5 * 60_000;
export const TASK_STATUS_TITLE_MAX_CHARS = 80;
export const TASK_STATUS_DETAIL_MAX_CHARS = 120;
function isActiveTask(task: TaskRecord): boolean {
return ACTIVE_TASK_STATUSES.has(task.status);
@@ -31,6 +33,32 @@ function isRecentTerminalTask(task: TaskRecord, now: number): boolean {
return now - resolveTaskReferenceAt(task) <= TASK_STATUS_RECENT_WINDOW_MS;
}
function truncateTaskStatusText(value: string, maxChars: number): string {
const trimmed = value.trim();
if (trimmed.length <= maxChars) {
return trimmed;
}
return `${truncateUtf16Safe(trimmed, Math.max(0, maxChars - 1)).trimEnd()}`;
}
export function formatTaskStatusTitle(task: TaskRecord): string {
return truncateTaskStatusText(
task.label?.trim() || task.task.trim(),
TASK_STATUS_TITLE_MAX_CHARS,
);
}
export function formatTaskStatusDetail(task: TaskRecord): string | undefined {
const raw =
task.status === "running" || task.status === "queued"
? task.progressSummary?.trim()
: task.error?.trim() || task.terminalSummary?.trim();
if (!raw) {
return undefined;
}
return truncateTaskStatusText(raw, TASK_STATUS_DETAIL_MAX_CHARS);
}
export type TaskStatusSnapshot = {
latest?: TaskRecord;
focus?: TaskRecord;
@@ -47,11 +75,9 @@ export function buildTaskStatusSnapshot(
opts?: { now?: number },
): TaskStatusSnapshot {
const now = opts?.now ?? Date.now();
const reconciled = tasks
.map((task) => reconcileTaskRecordForOperatorInspection(task))
.filter((task) => !isExpiredTask(task, now));
const active = reconciled.filter(isActiveTask);
const recentTerminal = reconciled.filter((task) => isRecentTerminalTask(task, now));
const visibleCandidates = tasks.filter((task) => !isExpiredTask(task, now));
const active = visibleCandidates.filter(isActiveTask);
const recentTerminal = visibleCandidates.filter((task) => isRecentTerminalTask(task, now));
const visible = active.length > 0 ? [...active, ...recentTerminal] : recentTerminal;
const focus =
active[0] ?? recentTerminal.find((task) => isFailureTask(task)) ?? recentTerminal[0];