Browse Source

fix: disable timeout when fallback is disabled (#140)

* Fix: Tmux Session Leak - Complete Session Lifecycle Management

## Problem
When background tasks complete, tmux panes remained open and opencode attach
processes became orphaned, accumulating over time.

## Root Cause
Missing session lifecycle management:
1. No session.abort() on task completion
2. No session.abort() on cancellation
3. No session.deleted event handler
4. No graceful shutdown (Ctrl+C before kill)

## Solution
- Add session.abort() in completeTask() (single point of responsibility)
- Add handleSessionDeleted() for cleanup on session deletion
- Add onSessionDeleted() in TmuxSessionManager for pane cleanup
- Implement graceful shutdown: Ctrl+C before kill-pane
- Wire up event handlers in main dispatcher

## Code Quality
- Removed duplicate code in completeTask()
- Eliminated redundant session.abort() calls
- All 43 tests pass

## Documentation
- Updated AGENTS.md with session lifecycle section
- Updated docs/tmux-integration.md with troubleshooting

## Testing
- @explorer and @librarian tasks complete and close panes automatically
- Zero orphaned processes after task completion

Inspired by oh-my-opencode session management implementation.

* fix: Address Greptile review feedback

- Remove redundant resolver deletion in handleSessionDeleted()
- Add session tracking cleanup in completeTask() as fallback
- Prevents memory leak if session.deleted event doesn't fire

Fixes issues identified in latest Greptile review.

* chore: Add .aim/ to .gitignore for AI memory directory

* fix: disable timeout when fallback is disabled

- Remove max(120000) constraint on timeoutMs
- Allow timeoutMs: 0 to disable timeout entirely
- Auto-disable timeout when fallback.enabled = false
- Default remains 15000ms (unchanged)
Riccardo Sallusti 5 months ago
parent
commit
ea63b4540f
3 changed files with 16 additions and 3 deletions
  1. 6 0
      .gitignore
  2. 9 2
      src/background/background-manager.ts
  3. 1 1
      src/config/schema.ts

+ 6 - 0
.gitignore

@@ -46,6 +46,12 @@ local
 opencode
 oh-my-opencode
 
+# AI Memory
+.aim/
+
+# Planning docs (not for commit)
+TIMEOUT_PLAN.md
+
 # Python
 __pycache__/
 *.py[cod]

+ 9 - 2
src/background/background-manager.ts

@@ -254,6 +254,12 @@ export class BackgroundTaskManager {
     args: Parameters<OpencodeClient['session']['prompt']>[0],
     timeoutMs: number,
   ): Promise<void> {
+    // No timeout when fallback disabled (timeoutMs = 0)
+    if (timeoutMs <= 0) {
+      await this.client.session.prompt(args);
+      return;
+    }
+
     await Promise.race([
       this.client.session.prompt(args),
       new Promise<never>((_, reject) => {
@@ -340,9 +346,10 @@ export class BackgroundTaskManager {
         parts: [{ type: 'text' as const, text: task.prompt }],
       } as PromptBody) as unknown as PromptBody;
 
-      const timeoutMs =
-        this.config?.fallback?.timeoutMs ?? FALLBACK_FAILOVER_TIMEOUT_MS;
       const fallbackEnabled = this.config?.fallback?.enabled ?? true;
+      const timeoutMs = fallbackEnabled
+        ? (this.config?.fallback?.timeoutMs ?? FALLBACK_FAILOVER_TIMEOUT_MS)
+        : 0; // 0 = no timeout when fallback disabled
       const chain = fallbackEnabled
         ? this.resolveFallbackChain(task.agent)
         : [];

+ 1 - 1
src/config/schema.ts

@@ -125,7 +125,7 @@ export type BackgroundTaskConfig = z.infer<typeof BackgroundTaskConfigSchema>;
 
 export const FailoverConfigSchema = z.object({
   enabled: z.boolean().default(true),
-  timeoutMs: z.number().min(1000).max(120000).default(15000),
+  timeoutMs: z.number().min(0).default(15000),
   chains: FallbackChainsSchema.default({}),
 });