Skip to content

refactor(background): implement event-driven fire-and-forget tasks - #89

Merged
alvinunreal merged 6 commits into
alvinunreal:masterfrom
nghyane:master
Jan 26, 2026
Merged

refactor(background): implement event-driven fire-and-forget tasks#89
alvinunreal merged 6 commits into
alvinunreal:masterfrom
nghyane:master

Conversation

@nghyane

@nghyane nghyane commented Jan 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Refactor BackgroundTaskManager to implement true fire-and-forget background tasks with event-driven completion detection.

Key Changes

  • Fire-and-forget launch: Tasks now return in ~1ms without waiting for session creation
  • Event-driven completion: Uses session.status events instead of deprecated session.idle
  • Start queue with concurrency limit: Configurable limit (default: 10) to avoid overwhelming the system
  • Optional notification: Parent session can be notified when task completes
  • Simplified tool API: Removed sync/async mode, simplified to fire-and-forget only

Breaking Changes

  • Removed sync/async mode parameters from background_task
  • Removed exports: createSession, sendPrompt, pollSession, resolveSessionId, extractResponseText
  • Task state transitions changed (new pendingstartingrunning states)

Files Changed

  • src/background/background-manager.ts - Full refactor
  • src/tools/background.ts - Simplified API
  • src/background/tmux-session-manager.ts - Event-driven cleanup
  • src/config/schema.ts - Added BackgroundTaskConfigSchema
  • src/index.ts - Registered session.status hooks

- BackgroundTaskManager now launches tasks in ~1ms (fire-and-forget)
- Uses session.status events instead of deprecated session.idle
- Adds start queue with configurable concurrency limit (default: 10)
- Optional notification to parent session on completion
- Removes sync mode and legacy polling-based approach
- Simplified tool API: background_task, background_output, background_cancel

BREAKING CHANGE: Removed sync/async mode, session.create, sendPrompt,
pollSession, resolveSessionId, extractResponseText exports
Copilot AI review requested due to automatic review settings January 26, 2026 14:12
@greptile-apps

greptile-apps Bot commented Jan 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Overview

Greptile Summary

This PR refactors BackgroundTaskManager to implement true fire-and-forget background tasks with event-driven completion detection, replacing the previous polling-based approach.

Key architectural improvements:

  • Fire-and-forget launch: launch() now returns synchronously in ~1ms with a pending task, while session creation happens asynchronously in a background queue
  • Event-driven completion: Uses session.status events instead of deprecated session.idle for completion detection, eliminating polling overhead
  • Concurrency-limited start queue: Tasks queue with configurable limit (default: 10) to prevent overwhelming the system during session creation
  • Simplified API: Removed sync/async mode complexity, focusing on single fire-and-forget pattern

Critical issue found:

  • Memory leak in completeTask() - the tasksBySessionId map (line 200) is never cleaned up when tasks complete, causing unbounded memory growth over time

Previous review issues addressed:

  • Fixed early return leak (line 176-182): Now properly decrements activeStarts and processes queue via finally block
  • Added resolver cleanup in cancel() to prevent memory leak
  • Pre-marked status as cancelled before calling completeTask() to handle race conditions

The refactor significantly improves the architecture by eliminating polling and making task launches truly non-blocking. However, the memory leak must be addressed before merging.

Confidence Score: 3/5

  • This PR has architectural improvements but contains a memory leak that will cause issues in production
  • Score reflects the memory leak in completeTask() where tasksBySessionId map is never cleaned up. While previous review issues were properly addressed and the refactor is well-architected, the memory leak is a critical defect that will cause unbounded memory growth. Once fixed, this would be a 4-5/5.
  • Pay close attention to src/background/background-manager.ts - fix the memory leak in completeTask() by adding tasksBySessionId.delete()

Important Files Changed

Filename Overview
src/background/background-manager.ts Refactored to fire-and-forget with event-driven completion. Memory leak: tasksBySessionId map never cleaned up when tasks complete. Early return fix applied correctly.
src/tools/background.ts Simplified API to fire-and-forget only. Removed sync/async mode parameters. Clean implementation with no issues found.
src/background/tmux-session-manager.ts Updated to use session.status events instead of deprecated session.idle. Event-driven cleanup properly implemented.
src/index.ts Registered session.status hooks for both BackgroundTaskManager and TmuxSessionManager. Proper event wiring.

Sequence Diagram

sequenceDiagram
    participant User
    participant BackgroundTool as background_task tool
    participant Manager as BackgroundTaskManager
    participant Queue as Start Queue
    participant Client as OpenCode Client
    participant EventSystem as Event System
    
    User->>BackgroundTool: Call background_task(prompt, agent)
    BackgroundTool->>Manager: launch(opts)
    
    Note over Manager: Phase A (synchronous)
    Manager->>Manager: Create task record<br/>(status: 'pending')
    Manager->>Queue: enqueueStart(task)
    Manager->>Queue: processQueue()
    Manager-->>BackgroundTool: Return task immediately<br/>(~1ms)
    BackgroundTool-->>User: task_id
    
    Note over Manager,Queue: Phase B (asynchronous)
    Queue->>Manager: startTask(task)<br/>(if activeStarts < maxConcurrent)
    Manager->>Manager: Set status = 'starting'<br/>activeStarts++
    
    alt Task cancelled during race
        Manager->>Manager: completeTask('cancelled')
        Manager->>Manager: activeStarts--<br/>processQueue()
    else Normal flow
        Manager->>Client: session.create()
        Client-->>Manager: sessionId
        Manager->>Manager: Set status = 'running'<br/>tasksBySessionId[sessionId] = taskId
        Manager->>Client: session.prompt()
        Manager->>Manager: activeStarts--<br/>processQueue()
        
        Note over Client: Agent executes task...
        
        Client->>EventSystem: session.status event<br/>(type: 'idle')
        EventSystem->>Manager: handleSessionStatus(event)
        Manager->>Manager: Check task status === 'running'
        Manager->>Client: session.messages()
        Client-->>Manager: assistant messages
        Manager->>Manager: extractAndCompleteTask()
        Manager->>Manager: completeTask('completed', result)
        
        opt notifyOnComplete enabled
            Manager->>Client: session.prompt(parentSessionId)<br/>[Notification message]
        end
        
        Manager->>Manager: Resolve completionResolvers
    end
    
    User->>BackgroundTool: background_output(task_id)
    BackgroundTool->>Manager: getResult(task_id)
    Manager-->>BackgroundTool: task object
    BackgroundTool-->>User: Result or status
Loading

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment thread src/background/background-manager.ts Outdated
Comment on lines +438 to +441
task.status = 'cancelled';
task.error = 'Cancelled by user';
task.completedAt = new Date();
this.completeTask(task, 'cancelled', 'Cancelled by user');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant status and completedAt assignment before calling completeTask. The completeTask method already sets these fields on lines 323-324, so setting them here creates duplicate logic.

Suggested change
task.status = 'cancelled';
task.error = 'Cancelled by user';
task.completedAt = new Date();
this.completeTask(task, 'cancelled', 'Cancelled by user');
this.completeTask(task, 'cancelled', 'Cancelled by user');
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/background/background-manager.ts
Line: 438:441

Comment:
Redundant status and completedAt assignment before calling `completeTask`. The `completeTask` method already sets these fields on lines 323-324, so setting them here creates duplicate logic.

```suggestion
        this.completeTask(task, 'cancelled', 'Cancelled by user');
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/background/background-manager.ts Outdated
Comment on lines +462 to +465
task.status = 'cancelled';
task.error = 'Cancelled by user';
task.completedAt = new Date();
this.completeTask(task, 'cancelled', 'Cancelled by user');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant status and completedAt assignment before calling completeTask (same issue as above).

Suggested change
task.status = 'cancelled';
task.error = 'Cancelled by user';
task.completedAt = new Date();
this.completeTask(task, 'cancelled', 'Cancelled by user');
this.completeTask(task, 'cancelled', 'Cancelled by user');
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/background/background-manager.ts
Line: 462:465

Comment:
Redundant status and completedAt assignment before calling `completeTask` (same issue as above).

```suggestion
        this.completeTask(task, 'cancelled', 'Cancelled by user');
```

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps

greptile-apps Bot commented Jan 26, 2026

Copy link
Copy Markdown
Contributor
Additional Comments (1)

src/background/tmux-session-manager.ts
Polling is never initiated - startPolling() is defined but never called anywhere in the code. This means the fallback reliability mechanism mentioned in the comments (lines 160-161) won't actually work. Sessions will rely entirely on session.status events, and timeout/missing session detection won't occur.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/background/tmux-session-manager.ts
Line: 141:157

Comment:
Polling is never initiated - `startPolling()` is defined but never called anywhere in the code. This means the fallback reliability mechanism mentioned in the comments (lines 160-161) won't actually work. Sessions will rely entirely on `session.status` events, and timeout/missing session detection won't occur.

How can I resolve this? If you propose a fix, please make it concise.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request refactors the BackgroundTaskManager to implement true fire-and-forget background tasks with event-driven completion detection.

Changes:

  • Removes synchronous execution mode from background tasks, simplifying to fire-and-forget only
  • Implements event-driven completion detection using session.status events instead of deprecated session.idle
  • Adds a start queue with configurable concurrency limit (default: 10) to avoid overwhelming the system
  • Adds optional notification feature to alert parent sessions when background tasks complete
  • Adds new BackgroundTaskConfig schema for configuration

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/tools/background.ts Simplified tool API - removed sync/async modes, updated to fire-and-forget pattern
src/tools/background.test.ts Removed entire test file (591 lines) - tests moved to manager level
src/index.ts Added event hooks for session.status to enable event-driven completion detection
src/config/schema.ts Added BackgroundTaskConfig schema with notifyOnComplete and maxConcurrentStarts options
src/background/tmux-session-manager.ts Added onSessionStatus handler for event-driven pane cleanup
src/background/background-manager.ts Complete refactor with fire-and-forget launch, start queue, and event-driven completion
src/background/background-manager.test.ts Updated tests to match new fire-and-forget behavior and event-driven approach
Comments suppressed due to low confidence (1)

src/tools/background.test.ts:1

  • The entire test file for the background tools was removed (591 lines), eliminating test coverage for the tool layer including background_task, background_output, and background_cancel tools. While the BackgroundTaskManager has good test coverage, the tools that wrap it are no longer tested. Consider adding tests to verify the tool argument validation, error handling, and integration with the manager.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/background/background-manager.ts Outdated
Comment on lines +462 to +465
task.status = 'cancelled';
task.error = 'Cancelled by user';
task.completedAt = new Date();
this.completeTask(task, 'cancelled', 'Cancelled by user');

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task status, error, and completedAt are being set manually before calling completeTask, which also sets these same fields. This is redundant and could lead to confusion or maintenance issues. Consider calling completeTask directly without pre-setting these fields, as completeTask already handles setting status, error/result, and completedAt.

Copilot uses AI. Check for mistakes.
Comment on lines +402 to +412
return new Promise((resolve) => {
const resolver = (t: BackgroundTask) => resolve(t);
this.completionResolvers.set(taskId, resolver);

if (timeout > 0) {
setTimeout(() => {
this.completionResolvers.delete(taskId);
resolve(this.tasks.get(taskId) ?? null);
}, timeout);
}
});

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When timeout is 0 (no timeout), the completion resolver is added to the map but will never be removed if the task never completes (e.g., if it's stuck or events are missed). This creates a memory leak. Consider adding a default maximum timeout or cleaning up resolvers when tasks are explicitly cancelled or the manager is shut down.

Copilot uses AI. Check for mistakes.
Comment thread src/background/background-manager.ts Outdated
Comment on lines +228 to +231
task.status = 'failed';
task.error = error instanceof Error ? error.message : String(error);
task.completedAt = new Date();
this.completeTask(task, 'failed', task.error);

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task status, error, and completedAt are being set manually before calling completeTask, which also sets these same fields. This is redundant and could lead to confusion or maintenance issues. Consider calling completeTask directly without pre-setting these fields, as completeTask already handles setting status, error/result, and completedAt.

Suggested change
task.status = 'failed';
task.error = error instanceof Error ? error.message : String(error);
task.completedAt = new Date();
this.completeTask(task, 'failed', task.error);
const errorMessage =
error instanceof Error ? error.message : String(error);
this.completeTask(task, 'failed', errorMessage);

Copilot uses AI. Check for mistakes.
Comment thread src/background/tmux-session-manager.ts Outdated
Comment on lines 229 to 249
/**
* Create the event handler for the plugin's event hook.
* Create the event handler for session.created events.
*/
createSessionCreatedHandler(): (input: {
event: SessionEvent;
}) => Promise<void> {
return async (input) => {
await this.onSessionCreated(input.event);
};
}

/**
* Create the event handler for session.status events.
*/
createEventHandler(): (input: {
event: { type: string; properties?: unknown };
createSessionStatusHandler(): (input: {
event: SessionEvent;
}) => Promise<void> {
return async (input) => {
await this.onSessionCreated(input.event as SessionCreatedEvent);
await this.onSessionStatus(input.event);
};
}

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The methods createSessionCreatedHandler and createSessionStatusHandler are not used anywhere in the codebase. They appear to be unnecessary wrappers around onSessionCreated and onSessionStatus methods. Consider removing these unused methods to reduce code complexity and maintenance burden.

Copilot uses AI. Check for mistakes.
Comment thread src/tools/background.ts Outdated
task &&
timeout > 0 &&
task.status !== 'completed' &&
task.status !== 'failed'

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition on line 114-115 checks if the task is not 'completed' or 'failed', but doesn't check for 'cancelled'. This means if a task is cancelled while waiting, the code will still try to wait for it. Consider adding task.status !== 'cancelled' to the condition to avoid unnecessary waiting for cancelled tasks.

Suggested change
task.status !== 'failed'
task.status !== 'failed' &&
task.status !== 'cancelled'

Copilot uses AI. Check for mistakes.
Comment thread src/background/background-manager.ts Outdated
@@ -57,6 +67,7 @@ export interface LaunchOptions {
description: string; // Human-readable task description
parentSessionId: string; // Parent session ID for task hierarchy
model?: string; // Optional model override

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model parameter in LaunchOptions (line 69) is defined but never used. The old implementation supported passing a model override to the prompt query, but this functionality has been removed. Either remove the unused model field from the interface, or add support for it by storing it in the task object and using it in startTask when building the prompt query (line 210).

Suggested change
model?: string; // Optional model override

Copilot uses AI. Check for mistakes.
Comment thread src/background/background-manager.ts Outdated
Comment on lines +438 to +441
task.status = 'cancelled';
task.error = 'Cancelled by user';
task.completedAt = new Date();
this.completeTask(task, 'cancelled', 'Cancelled by user');

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task status, error, and completedAt are being set manually before calling completeTask, which also sets these same fields. This is redundant and could lead to confusion or maintenance issues. Consider calling completeTask directly without pre-setting these fields, as completeTask already handles setting status, error/result, and completedAt.

Copilot uses AI. Check for mistakes.
Comment thread src/config/schema.ts
Comment on lines 42 to +43
// Main plugin config
// Background task configuration

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "Background task configuration" appears to be misplaced. It should be above the BackgroundTaskConfigSchema definition (line 44) rather than after the "Main plugin config" comment. Consider moving it to line 43 for better clarity.

Copilot uses AI. Check for mistakes.
Comment thread src/background/background-manager.ts Outdated
import type { PluginConfig } from '../config';
import { POLL_INTERVAL_BACKGROUND_MS, POLL_INTERVAL_SLOW_MS } from '../config';
import type { BackgroundTaskConfig, PluginConfig } from '../config';
import { POLL_INTERVAL_BACKGROUND_MS } from '../config';

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import POLL_INTERVAL_BACKGROUND_MS.

Suggested change
import { POLL_INTERVAL_BACKGROUND_MS } from '../config';

Copilot uses AI. Check for mistakes.
- TmuxSessionManager: call startPolling() for fallback reliability
- Remove redundant assignments before completeTask() in cancel()
- Remove unused model parameter from LaunchOptions
- Remove unused POLL_INTERVAL_BACKGROUND_MS import
- Remove unused createSessionCreatedHandler/createSessionStatusHandler methods
- Fix misplaced comment in schema.ts
@nghyane

nghyane commented Jan 26, 2026

Copy link
Copy Markdown
Contributor Author

All review comments have been addressed:

Fixed:

  • ✅ TmuxSessionManager: startPolling() now called for fallback reliability
  • ✅ Removed redundant assignments before completeTask() in cancel() method
  • ✅ Removed unused parameter from LaunchOptions
  • ✅ Removed unused POLL_INTERVAL_BACKGROUND_MS import
  • ✅ Removed unused createSessionCreatedHandler/createSessionStatusHandler methods
  • ✅ Fixed misplaced comment in schema.ts

Verified: All 23 tests pass.

@alvinunreal

Copy link
Copy Markdown
Owner

@greptile

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +316 to +318
if (task.status === 'completed' || task.status === 'failed') {
return; // Already completed
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guard should also check for cancelled status to prevent duplicate completion handling.

Suggested change
if (task.status === 'completed' || task.status === 'failed') {
return; // Already completed
}
if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
return; // Already completed
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/background/background-manager.ts
Line: 316:318

Comment:
Guard should also check for `cancelled` status to prevent duplicate completion handling.

```suggestion
    if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
      return; // Already completed
    }
```

How can I resolve this? If you propose a fix, please make it concise.

- completeTask: guard now checks cancelled status
- background.ts: waitForCompletion skips cancelled tasks
@nghyane

nghyane commented Jan 26, 2026

Copy link
Copy Markdown
Contributor Author

Additional fixes applied in commit 1d84c7c:

  • completeTask: guard now checks cancelled status
  • background.ts: waitForCompletion skips cancelled tasks

Verified: 23 tests pass.

@alvinunreal

Copy link
Copy Markdown
Owner

@greptile-apps

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +431 to +437
// Remove from start queue if pending
if (task.status === 'pending') {
const idx = this.startQueue.findIndex((t) => t.id === taskId);
if (idx >= 0) {
this.startQueue.splice(idx, 1);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: if task status changes from pending to starting between the status check (line 427-429) and queue removal (line 432-436), the task won't be removed from the queue but will still be cancelled. This could leave a stale reference in startQueue.

Consider checking task.status === 'pending' again after acquiring any necessary locks, or remove from queue unconditionally within the status check block.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/background/background-manager.ts
Line: 431:437

Comment:
Race condition: if task status changes from `pending` to `starting` between the status check (line 427-429) and queue removal (line 432-436), the task won't be removed from the queue but will still be cancelled. This could leave a stale reference in `startQueue`.

Consider checking `task.status === 'pending'` again after acquiring any necessary locks, or remove from queue unconditionally within the status check block.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +422 to +443
cancel(taskId?: string): number {
if (taskId) {
const task = this.tasks.get(taskId);
if (
task &&
(task.status === 'pending' ||
task.status === 'starting' ||
task.status === 'running')
) {
// Remove from start queue if pending
if (task.status === 'pending') {
const idx = this.startQueue.findIndex((t) => t.id === taskId);
if (idx >= 0) {
this.startQueue.splice(idx, 1);
}
}

this.completeTask(task, 'cancelled', 'Cancelled by user');
return 1;
}
return 0;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When cancelling a task with status === 'running' (line 429), the background session is not terminated. The session will continue executing until it naturally completes, at which point the completion will be ignored (line 252 checks task.status !== 'running').

Consider calling client.session.delete() or similar to terminate the session and free resources immediately.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/background/background-manager.ts
Line: 422:443

Comment:
When cancelling a task with `status === 'running'` (line 429), the background session is not terminated. The session will continue executing until it naturally completes, at which point the completion will be ignored (line 252 checks `task.status !== 'running'`).

Consider calling `client.session.delete()` or similar to terminate the session and free resources immediately.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/background/background-manager.ts Outdated
Comment on lines 176 to 179
if (task.status === 'cancelled') {
this.completeTask(task, 'cancelled', 'Task cancelled before start');
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The early return here prevents activeStarts from being decremented and processQueue() from being called (which happen in the finally block at line 230-231). This means if a task is cancelled while in the queue but after being shifted from it (line 166), the active start slot is never released.

Suggested change
if (task.status === 'cancelled') {
this.completeTask(task, 'cancelled', 'Task cancelled before start');
return;
}
// Check if cancelled before starting
if (task.status === 'cancelled') {
this.activeStarts--;
this.completeTask(task, 'cancelled', 'Task cancelled before start');
this.processQueue();
return;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/background/background-manager.ts
Line: 176:179

Comment:
The early return here prevents `activeStarts` from being decremented and `processQueue()` from being called (which happen in the finally block at line 230-231). This means if a task is cancelled while in the queue but after being shifted from it (line 166), the active start slot is never released.

```suggestion
    // Check if cancelled before starting
    if (task.status === 'cancelled') {
      this.activeStarts--;
      this.completeTask(task, 'cancelled', 'Task cancelled before start');
      this.processQueue();
      return;
    }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +198 to +200
task.sessionId = session.data.id;
this.tasksBySessionId.set(session.data.id, task.id);
task.status = 'running';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If task is cancelled after session creation but before setting running status, handleSessionStatus won't clean up because it checks task.status !== 'running' (line 252). The tasksBySessionId entry will persist even though the task is cancelled.

Consider also checking for cancelled status in handleSessionStatus or cleaning up tasksBySessionId in completeTask.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/background/background-manager.ts
Line: 198:200

Comment:
If task is cancelled after session creation but before setting `running` status, `handleSessionStatus` won't clean up because it checks `task.status !== 'running'` (line 252). The `tasksBySessionId` entry will persist even though the task is cancelled.

Consider also checking for cancelled status in `handleSessionStatus` or cleaning up `tasksBySessionId` in `completeTask`.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +355 to +369
private async sendCompletionNotification(
task: BackgroundTask,
): Promise<void> {
const message =
task.status === 'completed'
? `[Background task "${task.description}" completed]`
: `[Background task "${task.description}" failed: ${task.error}]`;

await this.client.session.prompt({
path: { id: task.parentSessionId },
body: {
parts: [{ type: 'text' as const, text: message }],
},
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notification failure is logged but not surfaced to the user. If session.prompt fails (e.g., parent session no longer exists), the task will be marked complete but the user won't be notified as expected.

Consider storing notification failure status on the task or retrying the notification.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/background/background-manager.ts
Line: 355:369

Comment:
Notification failure is logged but not surfaced to the user. If `session.prompt` fails (e.g., parent session no longer exists), the task will be marked complete but the user won't be notified as expected.

Consider storing notification failure status on the task or retrying the notification.

How can I resolve this? If you propose a fix, please make it concise.

nghyane and others added 2 commits January 26, 2026 22:15
- Fix activeStarts leak in startTask early return
- Add resolver cleanup to prevent memory leak
- Fix race condition in cancel() by marking cancelled first
@alvinunreal

Copy link
Copy Markdown
Owner

@greptile-apps

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread src/background/background-manager.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@alvinunreal
alvinunreal merged commit d2c326f into alvinunreal:master Jan 26, 2026
1 check passed
@alvinunreal

Copy link
Copy Markdown
Owner

Thanks @nghyane good one

nghyane added a commit to nghyane/oh-my-opencode-slim that referenced this pull request Jan 31, 2026
…lvinunreal#89)

* refactor(background): implement event-driven fire-and-forget tasks

- BackgroundTaskManager now launches tasks in ~1ms (fire-and-forget)
- Uses session.status events instead of deprecated session.idle
- Adds start queue with configurable concurrency limit (default: 10)
- Optional notification to parent session on completion
- Removes sync mode and legacy polling-based approach
- Simplified tool API: background_task, background_output, background_cancel

BREAKING CHANGE: Removed sync/async mode, session.create, sendPrompt,
pollSession, resolveSessionId, extractResponseText exports

* fix(PR#89): address review comments

- TmuxSessionManager: call startPolling() for fallback reliability
- Remove redundant assignments before completeTask() in cancel()
- Remove unused model parameter from LaunchOptions
- Remove unused POLL_INTERVAL_BACKGROUND_MS import
- Remove unused createSessionCreatedHandler/createSessionStatusHandler methods
- Fix misplaced comment in schema.ts

* fix(PR#89): add cancelled status checks

- completeTask: guard now checks cancelled status
- background.ts: waitForCompletion skips cancelled tasks

* fix(PR#89): address remaining review comments
- Fix activeStarts leak in startTask early return
- Add resolver cleanup to prevent memory leak
- Fix race condition in cancel() by marking cancelled first

* Update src/background/background-manager.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: “nghyane” <“hoangvananhnghia99@gmail.com”>
Co-authored-by: Alvin <alvin@cmngoal.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
mhenke pushed a commit to mhenke/oh-my-opencode-slim that referenced this pull request Jul 17, 2026
…lvinunreal#89)

* refactor(background): implement event-driven fire-and-forget tasks

- BackgroundTaskManager now launches tasks in ~1ms (fire-and-forget)
- Uses session.status events instead of deprecated session.idle
- Adds start queue with configurable concurrency limit (default: 10)
- Optional notification to parent session on completion
- Removes sync mode and legacy polling-based approach
- Simplified tool API: background_task, background_output, background_cancel

BREAKING CHANGE: Removed sync/async mode, session.create, sendPrompt,
pollSession, resolveSessionId, extractResponseText exports

* fix(PR#89): address review comments

- TmuxSessionManager: call startPolling() for fallback reliability
- Remove redundant assignments before completeTask() in cancel()
- Remove unused model parameter from LaunchOptions
- Remove unused POLL_INTERVAL_BACKGROUND_MS import
- Remove unused createSessionCreatedHandler/createSessionStatusHandler methods
- Fix misplaced comment in schema.ts

* fix(PR#89): add cancelled status checks

- completeTask: guard now checks cancelled status
- background.ts: waitForCompletion skips cancelled tasks

* fix(PR#89): address remaining review comments
- Fix activeStarts leak in startTask early return
- Add resolver cleanup to prevent memory leak
- Fix race condition in cancel() by marking cancelled first

* Update src/background/background-manager.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: “nghyane” <“hoangvananhnghia99@gmail.com”>
Co-authored-by: Alvin <alvin@cmngoal.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants