diff --git a/src/features/tmux-installer/main/composition/runtimeSupport.ts b/src/features/tmux-installer/main/composition/runtimeSupport.ts index 89d00b9a..3a769591 100644 --- a/src/features/tmux-installer/main/composition/runtimeSupport.ts +++ b/src/features/tmux-installer/main/composition/runtimeSupport.ts @@ -1,5 +1,6 @@ import { TmuxStatusSourceAdapter } from '../adapters/output/sources/TmuxStatusSourceAdapter'; import { + type ListRuntimeProcessesOptions, type RuntimeProcessTableRow, type TmuxPaneRuntimeInfo, TmuxPlatformCommandExecutor, @@ -34,10 +35,10 @@ export async function listTmuxPaneRuntimeInfoForCurrentPlatform( return runtimeCommandExecutor.listPaneRuntimeInfo(paneIds); } -export async function listRuntimeProcessTableForCurrentPlatform(): Promise< - RuntimeProcessTableRow[] -> { - return runtimeCommandExecutor.listRuntimeProcesses(); +export async function listRuntimeProcessTableForCurrentPlatform( + options: ListRuntimeProcessesOptions = {} +): Promise { + return runtimeCommandExecutor.listRuntimeProcesses(options); } export async function sendKeysToTmuxPaneForCurrentPlatform( diff --git a/src/features/tmux-installer/main/infrastructure/runtime/TmuxPlatformCommandExecutor.ts b/src/features/tmux-installer/main/infrastructure/runtime/TmuxPlatformCommandExecutor.ts index 7d3f66f3..aee9b701 100644 --- a/src/features/tmux-installer/main/infrastructure/runtime/TmuxPlatformCommandExecutor.ts +++ b/src/features/tmux-installer/main/infrastructure/runtime/TmuxPlatformCommandExecutor.ts @@ -32,6 +32,10 @@ export interface RuntimeProcessTableRow { rssBytes?: number; } +export interface ListRuntimeProcessesOptions { + bypassCache?: boolean; +} + /** * Short-lived cache window for the global process table. * @@ -218,9 +222,11 @@ export class TmuxPlatformCommandExecutor { return new Map([...info.entries()].map(([paneId, pane]) => [paneId, pane.panePid])); } - async listRuntimeProcesses(): Promise { + async listRuntimeProcesses( + options: ListRuntimeProcessesOptions = {} + ): Promise { const cached = this.#runtimeProcessTableCache; - if (cached && cached.expiresAtMs > Date.now()) { + if (options.bypassCache !== true && cached && cached.expiresAtMs > Date.now()) { return cached.rows; } if (this.#runtimeProcessTableInFlight) { diff --git a/src/features/tmux-installer/main/infrastructure/runtime/__tests__/TmuxPlatformCommandExecutor.test.ts b/src/features/tmux-installer/main/infrastructure/runtime/__tests__/TmuxPlatformCommandExecutor.test.ts index 6d811aa1..41cc104e 100644 --- a/src/features/tmux-installer/main/infrastructure/runtime/__tests__/TmuxPlatformCommandExecutor.test.ts +++ b/src/features/tmux-installer/main/infrastructure/runtime/__tests__/TmuxPlatformCommandExecutor.test.ts @@ -152,4 +152,39 @@ describe('TmuxPlatformCommandExecutor', () => { ]); expect(childProcess.execFile).not.toHaveBeenCalled(); }); + + it('can bypass the runtime process table cache for fresh process reads', async () => { + setPlatform('win32'); + const execInPreferredDistro = vi + .fn() + .mockResolvedValueOnce({ + exitCode: 0, + stdout: ' 42 1 1.0 128 opencode runtime --team-name demo --agent-id alice@demo\n', + stderr: '', + }) + .mockResolvedValueOnce({ + exitCode: 0, + stdout: ' 43 1 1.0 128 opencode runtime --team-name demo --agent-id alice@demo\n', + stderr: '', + }); + const executor = new TmuxPlatformCommandExecutor( + { + execInPreferredDistro, + getPersistedPreferredDistroSync: () => 'Ubuntu', + } as never, + {} as never + ); + + await expect(executor.listRuntimeProcesses()).resolves.toEqual([ + expect.objectContaining({ pid: 42 }), + ]); + await expect(executor.listRuntimeProcesses()).resolves.toEqual([ + expect.objectContaining({ pid: 42 }), + ]); + await expect(executor.listRuntimeProcesses({ bypassCache: true })).resolves.toEqual([ + expect.objectContaining({ pid: 43 }), + ]); + + expect(execInPreferredDistro).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index b0e4dd34..dc07d9e6 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -10522,6 +10522,7 @@ export class TeamProvisioningService { peekAutoResumeService()?.cancelPendingAutoResume(teamName); this.clearOpenCodeRuntimeToolApprovals(teamName, { emitDismiss: true }); this.invalidateRuntimeSnapshotCaches(teamName); + this.runtimeProcessRowsForUsageSnapshotByTeam.delete(teamName); this.retainedClaudeLogsByTeam.delete(teamName); this.persistedTranscriptClaudeLogsCache.delete(teamName); this.leadInboxRelayInFlight.delete(teamName); @@ -25554,7 +25555,10 @@ export class TeamProvisioningService { } } } - if (processRowsReadForMetadata) { + if ( + processRowsReadForMetadata && + this.getRuntimeSnapshotCacheGeneration(teamName) === generationAtStart + ) { const sampledAtMs = Date.now(); this.runtimeProcessRowsForUsageSnapshotByTeam.set(teamName, { expiresAtMs: diff --git a/src/main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup.ts b/src/main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup.ts index 70ff30b4..cdb2b565 100644 --- a/src/main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup.ts +++ b/src/main/services/team/opencode/bridge/OpenCodeManagedHostProcessCleanup.ts @@ -60,10 +60,12 @@ export async function cleanupManagedOpenCodeServeProcesses( diagnostics: [], }; - const rows = await ( + const listProcessRows = options.listProcessRows ?? - (platform === 'win32' ? listWindowsProcessTable : listRuntimeProcessTableForCurrentPlatform) - )(); + (platform === 'win32' + ? listWindowsProcessTable + : () => listRuntimeProcessTableForCurrentPlatform({ bypassCache: true })); + const rows = await listProcessRows(); const excludePids = options.excludePids ?? new Set(); const requiredDetailsMarkers = options.requiredDetailsMarkers ?? []; const readDetails =