From 59a78d62a1fe776a7ce6cfec2e5550619900341a Mon Sep 17 00:00:00 2001 From: Amit-Amitt Date: Sun, 2 Aug 2026 19:14:24 +0530 Subject: [PATCH] fix(core): reset task TTL on activity --- .../core/src/core/__tests__/task.test.ts | 53 +++++++++++++++++++ typescript/packages/core/src/core/task.ts | 4 +- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/typescript/packages/core/src/core/__tests__/task.test.ts b/typescript/packages/core/src/core/__tests__/task.test.ts index ba9330627..45763086e 100644 --- a/typescript/packages/core/src/core/__tests__/task.test.ts +++ b/typescript/packages/core/src/core/__tests__/task.test.ts @@ -279,6 +279,59 @@ describe('TaskManager', () => { }); }); + // ── TTL cleanup ────────────────────────────────────────────────────────────── + describe('TTL cleanup', () => { + it('keeps an active task alive when progress resets its TTL', () => { + jest.useFakeTimers(); + + const ttlManager = new TaskManager({ + logger: createMockLogger(), + defaultTtl: 60000, + defaultPollInterval: 1000, + }); + + try { + const task = ttlManager.createTask({ ttl: 60000 }); + const ctx = new TaskContext(ttlManager, task.taskId); + + // Report progress 50 seconds after creation. + jest.advanceTimersByTime(50000); + ctx.updateProgress('Still working'); + + // Reach the cleanup sweep at 90 seconds. + // Task is 90s old, but only 40s idle. + jest.advanceTimersByTime(40000); + + expect(ttlManager.hasTask(task.taskId)).toBe(true); + } finally { + ttlManager.destroy(); + jest.useRealTimers(); + } + }); + it('removes a task after its TTL when there is no activity', () => { + jest.useFakeTimers(); + + const ttlManager = new TaskManager({ + logger: createMockLogger(), + defaultTtl: 60000, + defaultPollInterval: 1000, + }); + + try { + const task = ttlManager.createTask({ ttl: 60000 }); + + // Cleanup runs every 30 seconds. + // At 90 seconds the task has been idle longer than its 60s TTL. + jest.advanceTimersByTime(90000); + + expect(ttlManager.hasTask(task.taskId)).toBe(false); + } finally { + ttlManager.destroy(); + jest.useRealTimers(); + } + }); + }); + // ── getAbortSignal ────────────────────────────────────────────────────────── describe('getAbortSignal', () => { it('returns an AbortSignal that is not yet aborted', () => { diff --git a/typescript/packages/core/src/core/task.ts b/typescript/packages/core/src/core/task.ts index 87b3c573d..fc05ca20b 100644 --- a/typescript/packages/core/src/core/task.ts +++ b/typescript/packages/core/src/core/task.ts @@ -336,8 +336,8 @@ export class TaskManager { for (const [taskId, entry] of this.tasks.entries()) { if (entry.data.ttl === null) continue; // Unlimited TTL - const createdTime = new Date(entry.data.createdAt).getTime(); - if (now - createdTime > entry.data.ttl) { + const lastActivityTime = new Date(entry.data.lastUpdatedAt).getTime(); + if (now - lastActivityTime > entry.data.ttl) { this.tasks.delete(taskId); this.logger.debug(`Expired task cleaned up: ${taskId}`); }