diff --git a/packages/core/src/services/executionLifecycleService.test.ts b/packages/core/src/services/executionLifecycleService.test.ts index 137fa09ec8..b8e280d248 100644 --- a/packages/core/src/services/executionLifecycleService.test.ts +++ b/packages/core/src/services/executionLifecycleService.test.ts @@ -10,6 +10,7 @@ import { type ExecutionHandle, type ExecutionResult, } from './executionLifecycleService.js'; +import { InjectionService } from '../config/injectionService.js'; function createResult( overrides: Partial = {}, @@ -628,5 +629,59 @@ describe('ExecutionLifecycleService', () => { ExecutionLifecycleService.offBackgroundComplete(listener); }); + + it('injects directly into InjectionService when wired via setInjectionService', async () => { + const injectionService = new InjectionService(() => true); + ExecutionLifecycleService.setInjectionService(injectionService); + + const injectionListener = vi.fn(); + injectionService.onInjection(injectionListener); + + const handle = ExecutionLifecycleService.createExecution( + '', + undefined, + 'remote_agent', + (output) => `[Completed] ${output}`, + undefined, + 'inject', + ); + const executionId = handle.pid!; + + ExecutionLifecycleService.appendOutput(executionId, 'agent output'); + ExecutionLifecycleService.background(executionId); + await handle.result; + + ExecutionLifecycleService.completeExecution(executionId); + + expect(injectionListener).toHaveBeenCalledWith( + '[Completed] agent output', + 'background_completion', + ); + }); + + it('does not inject into InjectionService for silent behavior', async () => { + const injectionService = new InjectionService(() => true); + ExecutionLifecycleService.setInjectionService(injectionService); + + const injectionListener = vi.fn(); + injectionService.onInjection(injectionListener); + + const handle = ExecutionLifecycleService.createExecution( + '', + undefined, + 'none', + () => 'should not inject', + undefined, + 'silent', + ); + const executionId = handle.pid!; + + ExecutionLifecycleService.background(executionId); + await handle.result; + + ExecutionLifecycleService.completeExecution(executionId); + + expect(injectionListener).not.toHaveBeenCalled(); + }); }); }); diff --git a/packages/core/src/services/executionLifecycleService.ts b/packages/core/src/services/executionLifecycleService.ts index a66c4bd000..cf61f8cc71 100644 --- a/packages/core/src/services/executionLifecycleService.ts +++ b/packages/core/src/services/executionLifecycleService.ts @@ -155,6 +155,16 @@ const NON_PROCESS_EXECUTION_ID_START = 2_000_000_000; export class ExecutionLifecycleService { private static readonly EXIT_INFO_TTL_MS = 5 * 60 * 1000; private static nextExecutionId = NON_PROCESS_EXECUTION_ID_START; + private static injectionService: InjectionService | null = null; + + /** + * Connects the lifecycle service to the injection service so that + * backgrounded executions are reinjected into the model conversation + * directly from the backend — no UI hop needed. + */ + static setInjectionService(service: InjectionService): void { + this.injectionService = service; + } private static activeExecutions = new Map(); private static activeResolvers = new Map< @@ -271,6 +281,7 @@ export class ExecutionLifecycleService { this.backgroundCompletionListeners.clear(); this.injectionService = null; this.backgroundStartListeners.clear(); + this.injectionService = null; this.nextExecutionId = NON_PROCESS_EXECUTION_ID_START; } @@ -388,6 +399,15 @@ export class ExecutionLifecycleService { behavior !== 'silent' && execution.formatInjection ? execution.formatInjection(result.output, result.error) : null; + + // Inject directly into the model conversation from the backend. + if (injectionText && this.injectionService) { + this.injectionService.addInjection( + injectionText, + 'background_completion', + ); + } + const info: BackgroundCompletionInfo = { executionId, executionMethod: execution.executionMethod,