diff --git a/server/src/routes/task-time.ts b/server/src/routes/task-time.ts index a49e7712..b1c450c5 100644 --- a/server/src/routes/task-time.ts +++ b/server/src/routes/task-time.ts @@ -27,15 +27,8 @@ router.get( router.post( '/:id/time/start', asyncHandler(async (req, res) => { - const { task, stoppedTaskId } = await taskService.startTimer(req.params.id as string); - - // Broadcast WS event for the auto-stopped task (if any) so other clients refresh - if (stoppedTaskId) { - broadcastTaskChange('updated', stoppedTaskId); - } - // Broadcast WS event for the started task + const task = await taskService.startTimer(req.params.id as string); broadcastTaskChange('updated', task.id); - res.json(task); }) ); diff --git a/server/src/services/task-service.ts b/server/src/services/task-service.ts index d5aab605..196cb25b 100644 --- a/server/src/services/task-service.ts +++ b/server/src/services/task-service.ts @@ -680,48 +680,21 @@ export class TaskService { // ============ Time Tracking Methods ============ - /** - * Find the task that currently has a running timer (if any). - */ - async getRunningTimerTask(): Promise { - await this.initCache(); - for (const task of this.cache.values()) { - if (task.timeTracking?.isRunning) { - return task; - } - } - return null; - } - /** * Start a timer for a task. - * Enforces global exclusivity: only one timer may run at a time. - * If another task has a running timer, it is auto-stopped first. - * Returns { task, stoppedTaskId? } so the caller can broadcast both. + * Per-task exclusivity: only one timer per task (but multiple tasks can + * each have their own running timer — supports multi-agent workflows). */ - async startTimer(taskId: string): Promise<{ task: Task; stoppedTaskId?: string }> { + async startTimer(taskId: string): Promise { const task = await this.getTask(taskId); if (!task) { throw new NotFoundError('Task not found'); } - // Check if timer is already running on THIS task if (task.timeTracking?.isRunning) { throw new ConflictError('Timer is already running for this task'); } - // Global exclusivity: stop any other running timer first - let stoppedTaskId: string | undefined; - const runningTask = await this.getRunningTimerTask(); - if (runningTask && runningTask.id !== taskId) { - await this.stopTimer(runningTask.id); - stoppedTaskId = runningTask.id; - log.info( - { stoppedTaskId, startedTaskId: taskId }, - 'Auto-stopped timer for global exclusivity' - ); - } - const entryId = `time_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; const now = new Date().toISOString(); @@ -737,8 +710,7 @@ export class TaskService { activeEntryId: entryId, }; - const updated = (await this.updateTask(taskId, { timeTracking })) as Task; - return { task: updated, stoppedTaskId }; + return (await this.updateTask(taskId, { timeTracking })) as Task; } /** diff --git a/web/src/components/task/TimeTrackingSection.tsx b/web/src/components/task/TimeTrackingSection.tsx index 1f07818e..5fe0d409 100644 --- a/web/src/components/task/TimeTrackingSection.tsx +++ b/web/src/components/task/TimeTrackingSection.tsx @@ -103,9 +103,6 @@ export function TimeTrackingSection({ task }: TimeTrackingSectionProps) { const entries = timeTracking?.entries ?? []; const activeEntry = entries.find((e) => e.id === timeTracking?.activeEntryId); - // Global exclusivity: is another task's timer running? - const otherRunningTask = allTasks?.find((t) => t.id !== task.id && t.timeTracking?.isRunning); - // ── Handlers ── const handleStartStop = async () => { @@ -218,13 +215,8 @@ export function TimeTrackingSection({ task }: TimeTrackingSectionProps) { )} - ) : otherRunningTask ? ( - /* Another task has the active timer — Start is blocked */ - - Timer active on another task - ) : ( - /* No timer running anywhere — show Start */ + /* Timer not running on this task — show Start */