/
fleisar
/
agent-timetracker
Обзор
Документация
Войти
/
fleisar
/
agent-timetracker
Код
Запросы
0
Задачи
Вики
Пакеты
1
Релизы
2
CI/CD
Аналитика
Безопасность
main
src/domain/timeout.ts
181 строка
5 KB
Matvey Kuznetsov
fix: tighten lazy timeout boundary
02 июл 2026, 18:27
02 июл 2026, 18:27
4c985c7
Код
Авторство
О чём код?
import type { AuditEvent, Task, WorkSession } from "./types.js"; import { trackerError } from "./errors.js"; import type { AuditLog } from "../storage/auditLog.js"; import type { TrackerStore } from "../storage/types.js"; import { addSeconds, parseIsoTimestamp, secondsBetween } from "../util/time.js"; export type EvaluateTimeoutsInput = { taskId: string; at: string; }; export type EvaluateTimeoutsResult = { closedSessions: WorkSession[]; }; export interface TimeoutEvaluatorDependencies { store: TrackerStore; auditLog: AuditLog; newEventId: () => string; } function withoutActiveSessionIds(task: Task): Omit<Task, "active_session_ids"> { const { active_session_ids, ...taskWithoutActiveSessionIds } = task; void active_session_ids; return taskWithoutActiveSessionIds; } function getElapsedMs(startIso: string, endIso: string): number { return parseIsoTimestamp(endIso).getTime() - parseIsoTimestamp(startIso).getTime(); } function closeSession( session: WorkSession, idleTimeoutSeconds: number ): { session: WorkSession; endedAt: string; } { const endedAt = addSeconds(session.last_activity_at, idleTimeoutSeconds); return { session: { ...session, status: "closed", ended_at: endedAt, end_reason: "paused_due_to_idle", ended_by: "system" }, endedAt }; } type TimeoutClosure = { session: WorkSession; closedSession: WorkSession; endedAt: string; closedDurationSeconds: number; }; function buildTimeoutClosures(task: Task, sessions: WorkSession[], at: string): TimeoutClosure[] { const closures: TimeoutClosure[] = []; for (const session of sessions) { const elapsedMs = getElapsedMs(session.last_activity_at, at); if (elapsedMs <= task.idle_timeout_seconds * 1000) { continue; } const { session: closedSession, endedAt } = closeSession(session, task.idle_timeout_seconds); closures.push({ session, closedSession, endedAt, closedDurationSeconds: secondsBetween(session.started_at, endedAt) }); } return closures; } function buildAutoPausedEvents( task: Task, closures: TimeoutClosure[], at: string, newEventId: () => string ): AuditEvent[] { return closures.map((closure) => ({ event_id: newEventId(), type: "session_auto_paused", at, task_id: task.task_id, session_id: closure.closedSession.session_id, agent_id: closure.closedSession.agent_id, actor: "system", payload: { ended_at: closure.endedAt, end_reason: closure.closedSession.end_reason, ended_by: closure.closedSession.ended_by, total_work_seconds: closure.closedDurationSeconds } })); } async function appendAuditEvents(auditLog: AuditLog, events: AuditEvent[]): Promise<void> { for (const event of events) { await auditLog.append(event); } } export function createTimeoutEvaluator(deps: TimeoutEvaluatorDependencies) { return async function evaluateTimeouts(input: EvaluateTimeoutsInput): Promise<EvaluateTimeoutsResult> { const existingTask = deps.store.getTask(input.taskId); if (existingTask == null) { throw trackerError("task_not_found", `Task not found: ${input.taskId}`, { taskId: input.taskId }); } const existingActiveSessions = deps.store.listActiveSessions(input.taskId); const plannedClosures = buildTimeoutClosures(existingTask, existingActiveSessions, input.at); const auditEvents = buildAutoPausedEvents(existingTask, plannedClosures, input.at, deps.newEventId); await appendAuditEvents(deps.auditLog, auditEvents); const result = deps.store.transaction(() => { const task = deps.store.getTask(input.taskId); if (task == null) { throw trackerError("task_not_found", `Task not found: ${input.taskId}`, { taskId: input.taskId }); } const activeSessions = deps.store.listActiveSessions(input.taskId); const closedSessions: WorkSession[] = []; let totalWorkSeconds = 0; for (const session of activeSessions) { const elapsedMs = getElapsedMs(session.last_activity_at, input.at); if (elapsedMs <= task.idle_timeout_seconds * 1000) { continue; } const { session: closedSession, endedAt } = closeSession(session, task.idle_timeout_seconds); const closedDurationSeconds = secondsBetween(closedSession.started_at, endedAt); deps.store.updateSession(closedSession); closedSessions.push(closedSession); totalWorkSeconds += closedDurationSeconds; } const remainingActiveSessions = deps.store.listActiveSessions(task.task_id); if (closedSessions.length > 0) { const taskUpdate: Omit<Task, "active_session_ids"> = { ...withoutActiveSessionIds(task), total_work_seconds: task.total_work_seconds + totalWorkSeconds, status: task.status !== "completed" ? remainingActiveSessions.length > 0 ? "active" : "paused" : task.status }; deps.store.updateTask(taskUpdate); } else if (task.status !== "completed" && remainingActiveSessions.length === 0 && task.status !== "paused") { deps.store.updateTask({ ...withoutActiveSessionIds(task), status: "paused" }); } return { closedSessions }; }); return result; }; }