The "attach" verb makes it clear that the caller brings an existing process/handle, while "create" (createExecution) means the lifecycle service allocates and owns the execution from scratch.
5.6 KiB
Summary
Extract a generic ExecutionLifecycleService that owns background-execution state (create, stream, background, complete, kill) so that any tool — not just shell — can be backgrounded via Ctrl+B. This is a pure abstraction/renaming refactor with no new features; the follow-up PR (stacked on this branch) wires remote agents into the same lifecycle.
Details
Why this change
Background execution was deeply coupled to ShellExecutionService: resolver maps, listener maps, exit-info TTL, and the background() / subscribe() / onExit() API all lived inside the shell service. Adding Ctrl+B support for remote agents (or MCP tools, local agents, etc.) would have required either duplicating that machinery or reaching into shell internals. This PR pulls the lifecycle out into a standalone service that shell delegates to.
Recommended reading order
This is a large diff (13 files, ~1500 insertions) but the bulk is mechanical delegation. Read in this order:
-
executionLifecycleService.ts— the new abstraction. Two execution kinds:- virtual: tool calls
createExecution(), gets an ID + result promise, streams viaappendOutput(), completes viacompleteExecution(). Used by the upcoming remote-agent wiring. - external: shell calls
attachExecution(pid, {...hooks})to attach lifecycle tracking to a real OS process. Same lifecycle, but with write/kill/isActive hooks delegated back to the PTY/child_process owner. - Shared:
background()resolves the result promise early withbackgrounded: truebut keeps the execution active for continued streaming.subscribe()replays a snapshot for late joiners.onExit()fires on completion or replays from a 5-minute TTL cache.
- virtual: tool calls
-
shellExecutionService.ts— the main deletion site. All background-state maps (activeResolvers,activeListeners,exitedPidInfo) are removed. Shell now callsattachExecution()at spawn time andcompleteWithResult()at exit. The publicbackground(),subscribe(),onExit(),kill(),isActive(),writeInput()methods become one-line delegates. -
tool-executor.ts+coreToolHookTriggers.ts— thesetPidCallback→setExecutionIdCallbackrename chain.ToolExecutorno longer checksinstanceof ShellToolInvocation; every tool gets the callback.executeToolWithHookspasses it through toinvocation.execute(). -
tools.ts+shell.ts—ToolInvocation.execute()signature gains optionalsetExecutionIdCallback.BackgroundExecutionDatainterface +isBackgroundExecutionData/getBackgroundExecutionIdhelpers added. Shell tool populatesdata.executionIdalongside the existingdata.pid. -
useGeminiStream.ts+shellCommandProcessor.ts— UI generalization. TheactivePtyIdcomputation no longer filters onrequest.name === 'run_shell_command'; any executing tool with a numericpid(execution ID) qualifies for Ctrl+B. Background registration uses the coreisBackgroundExecutionData/getBackgroundExecutionIdhelpers. Parameter renamedactiveToolPtyId→activeBackgroundExecutionId.
Key design decisions
- Static class, not singleton instance:
ExecutionLifecycleServiceuses static methods and maps, matching the existingShellExecutionServicepattern. This avoids DI plumbing changes across the codebase. pidfield as backward-compat alias:ExecutionHandle.pidandBackgroundExecutionData.pidremain for existing shell consumers. New code should useexecutionId/getBackgroundExecutionId().NON_PROCESS_EXECUTION_ID_START = 2_000_000_000: Virtual execution IDs start above any realistic OS PID.isActive()short-circuits for IDs in this range to avoid spuriousprocess.kill()syscalls.- No
finalizeExecution: The deprecated alias was removed (zero callers).
Related Issues
How to Validate
-
Read the new service first:
packages/core/src/services/executionLifecycleService.ts— verify the create → stream → background → complete lifecycle makes sense in isolation. -
Verify shell delegation:
shellExecutionService.tsshould have no background-state maps of its own. Every public lifecycle method should be a one-liner delegating toExecutionLifecycleService. -
Run the targeted test suites:
npm run test --workspace @google/gemini-cli-core -- --run \ src/services/executionLifecycleService.test.ts \ src/services/shellExecutionService.test.ts \ src/core/coreToolHookTriggers.test.ts \ src/scheduler/tool-executor.test.ts npm run test --workspace @google/gemini-cli -- --run \ src/ui/hooks/useGeminiStream.test.tsx -
Typecheck and lint:
npm run typecheck --workspace @google/gemini-cli-core npm run lint --workspace @google/gemini-cli-core npm run lint --workspace @google/gemini-cli -
Verify no behavioral change: Existing shell backgrounding (Ctrl+B during a
run_shell_command) should work identically. The UI background panel,onExitnotifications, and kill behavior are unchanged.
Pre-Merge Checklist
- Updated relevant documentation and README (if needed)
- Added/updated tests (if needed)
- Noted breaking changes (if any)
- Validated on required platforms/methods:
- MacOS
- npm run
- npx
- Docker
- Podman
- Seatbelt
- Windows
- npm run
- npx
- Docker
- Linux
- npm run
- npx
- Docker
- MacOS