From b88ca42fe3d39989d1d297132a73285db0903d4b Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 09:12:05 +0300 Subject: [PATCH 01/59] fix(startup): serialize provider runtime checks --- src/main/ipc/cliInstaller.ts | 16 +++++++++--- test/main/ipc/cliInstaller.test.ts | 40 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/main/ipc/cliInstaller.ts b/src/main/ipc/cliInstaller.ts index 16d618f3..9775cc43 100644 --- a/src/main/ipc/cliInstaller.ts +++ b/src/main/ipc/cliInstaller.ts @@ -38,6 +38,7 @@ const logger = createLogger('IPC:cliInstaller'); let service: CliInstallerService; const statusInFlight = new Map>(); const providerStatusInFlight = new Map>(); +let providerRuntimeRequestTail: Promise = Promise.resolve(); const cachedStatus = new Map< CliInstallerProviderStatusMode, { value: CliInstallationStatus; at: number } @@ -110,11 +111,21 @@ function canUseStatusForCacheKey( ); } +function runProviderRuntimeRequest(operation: () => Promise): Promise { + const request = providerRuntimeRequestTail.then(operation, operation); + providerRuntimeRequestTail = request.then( + () => undefined, + () => undefined + ); + return request; +} + /** * Initializes CLI installer handlers with the service instance. */ export function initializeCliInstallerHandlers(installerService: CliInstallerService): void { service = installerService; + providerRuntimeRequestTail = Promise.resolve(); } /** @@ -255,8 +266,7 @@ async function handleGetProviderStatus( } const generation = statusCacheGeneration; - const request = service - .getProviderStatus(providerId) + const request = runProviderRuntimeRequest(() => service.getProviderStatus(providerId)) .then((status) => { if (generation === statusCacheGeneration) { patchCachedProviderStatus(status); @@ -296,7 +306,7 @@ async function handleVerifyProviderModels( ): Promise> { try { const generation = statusCacheGeneration; - const status = await service.verifyProviderModels(providerId); + const status = await runProviderRuntimeRequest(() => service.verifyProviderModels(providerId)); if (generation === statusCacheGeneration) { patchCachedProviderStatus(status); } diff --git a/test/main/ipc/cliInstaller.test.ts b/test/main/ipc/cliInstaller.test.ts index abf7878a..5e4ff5f9 100644 --- a/test/main/ipc/cliInstaller.test.ts +++ b/test/main/ipc/cliInstaller.test.ts @@ -221,6 +221,46 @@ describe('cliInstaller IPC handlers', () => { expect(service.invalidateStatusCache).toHaveBeenCalledTimes(1); }); + it('serializes explicit provider runtime status requests to avoid startup memory spikes', async () => { + const codexRequest = deferred(); + const opencodeRequest = deferred(); + const startedProviders: CliProviderId[] = []; + service.getProviderStatus.mockImplementation((providerId: CliProviderId) => { + startedProviders.push(providerId); + return providerId === 'codex' ? codexRequest.promise : opencodeRequest.promise; + }); + + const codexInvoke = ipcMain.invoke( + CLI_INSTALLER_GET_PROVIDER_STATUS, + 'codex' + ) as Promise>; + await vi.waitFor(() => expect(service.getProviderStatus).toHaveBeenCalledTimes(1)); + + const opencodeInvoke = ipcMain.invoke( + CLI_INSTALLER_GET_PROVIDER_STATUS, + 'opencode' + ) as Promise>; + await Promise.resolve(); + await Promise.resolve(); + + expect(startedProviders).toEqual(['codex']); + expect(service.getProviderStatus).toHaveBeenCalledTimes(1); + + codexRequest.resolve(provider({ providerId: 'codex', authenticated: true })); + await expect(codexInvoke).resolves.toMatchObject({ + success: true, + data: { providerId: 'codex' }, + }); + await vi.waitFor(() => expect(service.getProviderStatus).toHaveBeenCalledTimes(2)); + + expect(startedProviders).toEqual(['codex', 'opencode']); + opencodeRequest.resolve(provider({ providerId: 'opencode', authenticated: true })); + await expect(opencodeInvoke).resolves.toMatchObject({ + success: true, + data: { providerId: 'opencode' }, + }); + }); + it('does not reuse or recache a status request that was in flight before invalidation', async () => { const staleStatus = status([ provider({ From a8ac52b6f382db6e6c8df2a2888ea12175aeb9e1 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 10:32:46 +0300 Subject: [PATCH 02/59] perf(startup): dedupe opencode version probes --- .../OpenCodeRuntimeInstallerService.ts | 240 +++++++++++++++--- .../OpenCodeRuntimeInstallerService.test.ts | 114 +++++++++ 2 files changed, 321 insertions(+), 33 deletions(-) diff --git a/src/main/services/infrastructure/OpenCodeRuntimeInstallerService.ts b/src/main/services/infrastructure/OpenCodeRuntimeInstallerService.ts index fef3a0cb..7d2bbf49 100644 --- a/src/main/services/infrastructure/OpenCodeRuntimeInstallerService.ts +++ b/src/main/services/infrastructure/OpenCodeRuntimeInstallerService.ts @@ -27,6 +27,10 @@ const MAX_TARBALL_BYTES = 250 * 1024 * 1024; const MAX_BINARY_BYTES = 350 * 1024 * 1024; const FETCH_TIMEOUT_MS = 60_000; const VERSION_TIMEOUT_MS = 10_000; +const VERSION_PROBE_SUCCESS_CACHE_TTL_MS = 30_000; +const VERSION_PROBE_FAILURE_CACHE_TTL_MS = 5_000; +const RUNTIME_STATUS_SUCCESS_CACHE_TTL_MS = 30_000; +const RUNTIME_STATUS_FAILURE_CACHE_TTL_MS = 5_000; interface NpmPackageMetadata { name?: string; @@ -97,15 +101,8 @@ export async function resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath(): Prom if (!binaryPath) { return null; } - try { - await execCli(binaryPath, ['--version'], { - timeout: VERSION_TIMEOUT_MS, - windowsHide: true, - }); - return binaryPath; - } catch { - return null; - } + const version = await probeOpenCodeBinaryVersionCached(binaryPath); + return version.ok ? binaryPath : null; } function getExecutableName(): string { @@ -173,6 +170,31 @@ type VerifiedOpenCodeBinaryProbe = | { ok: true; binaryPath: string; version: string | null } | { ok: false; firstFailure: { binaryPath: string; error: string } | null }; +interface CachedVersionProbe { + result: OpenCodeBinaryVersionProbe; + cachedAt: number; + ttlMs: number; +} + +interface CachedPathProbe { + result: VerifiedOpenCodeBinaryProbe; + cachedAt: number; + ttlMs: number; +} + +interface CachedRuntimeBinaryResolve { + binaryPath: string | null; + cachedAt: number; + ttlMs: number; +} + +const versionProbeCache = new Map(); +const versionProbeInFlight = new Map>(); +const pathProbeCache = new Map(); +const pathProbeInFlight = new Map>(); +const runtimeBinaryResolveCache = new Map(); +const runtimeBinaryResolveInFlight = new Map>(); + async function probeOpenCodeBinaryVersion(binaryPath: string): Promise { try { const { stdout } = await execCli(binaryPath, ['--version'], { @@ -190,6 +212,42 @@ function normalizeBinaryCandidateForCompare(binaryPath: string): string { return process.platform === 'win32' ? normalized.toLowerCase() : normalized; } +function getVersionProbeTtlMs(result: OpenCodeBinaryVersionProbe): number { + return result.ok ? VERSION_PROBE_SUCCESS_CACHE_TTL_MS : VERSION_PROBE_FAILURE_CACHE_TTL_MS; +} + +async function probeOpenCodeBinaryVersionCached( + binaryPath: string +): Promise { + const cacheKey = normalizeBinaryCandidateForCompare(binaryPath); + const cached = versionProbeCache.get(cacheKey); + if (cached && Date.now() - cached.cachedAt < cached.ttlMs) { + return cached.result; + } + + const inFlight = versionProbeInFlight.get(cacheKey); + if (inFlight) { + return inFlight; + } + + const request = probeOpenCodeBinaryVersion(binaryPath) + .then((result) => { + versionProbeCache.set(cacheKey, { + result, + cachedAt: Date.now(), + ttlMs: getVersionProbeTtlMs(result), + }); + return result; + }) + .finally(() => { + if (versionProbeInFlight.get(cacheKey) === request) { + versionProbeInFlight.delete(cacheKey); + } + }); + versionProbeInFlight.set(cacheKey, request); + return request; +} + async function probeFirstWorkingOpenCodeBinaryCandidate( candidates: string[], seen: Set, @@ -202,7 +260,7 @@ async function probeFirstWorkingOpenCodeBinaryCandidate( continue; } seen.add(normalized); - const version = await probeOpenCodeBinaryVersion(binaryPath); + const version = await probeOpenCodeBinaryVersionCached(binaryPath); if (version.ok) { return { ok: true, binaryPath, version: version.version }; } @@ -217,6 +275,22 @@ interface OpenCodeRuntimeBinaryResolveOptions { includeShellEnv?: boolean; } +function getPathProbeCacheKey(options: OpenCodeRuntimeBinaryResolveOptions = {}): string { + if (options.includeShellEnv === false) { + return 'no-shell-env'; + } + + return `shell-env:${options.shellEnvTimeoutMs ?? RUNTIME_PATH_SHELL_ENV_TIMEOUT_MS}`; +} + +function getPathProbeTtlMs(result: VerifiedOpenCodeBinaryProbe): number { + return result.ok ? VERSION_PROBE_SUCCESS_CACHE_TTL_MS : VERSION_PROBE_FAILURE_CACHE_TTL_MS; +} + +function getRuntimeBinaryResolveTtlMs(binaryPath: string | null): number { + return binaryPath ? VERSION_PROBE_SUCCESS_CACHE_TTL_MS : VERSION_PROBE_FAILURE_CACHE_TTL_MS; +} + async function probeFirstWorkingPathOpenCodeBinary( options: OpenCodeRuntimeBinaryResolveOptions = {} ): Promise { @@ -268,20 +342,86 @@ async function probeFirstWorkingPathOpenCodeBinary( ); } +async function probeFirstWorkingPathOpenCodeBinaryCached( + options: OpenCodeRuntimeBinaryResolveOptions = {} +): Promise { + const cacheKey = getPathProbeCacheKey(options); + const cached = pathProbeCache.get(cacheKey); + if (cached && Date.now() - cached.cachedAt < cached.ttlMs) { + return cached.result; + } + + const inFlight = pathProbeInFlight.get(cacheKey); + if (inFlight) { + return inFlight; + } + + const request = probeFirstWorkingPathOpenCodeBinary(options) + .then((result) => { + pathProbeCache.set(cacheKey, { + result, + cachedAt: Date.now(), + ttlMs: getPathProbeTtlMs(result), + }); + return result; + }) + .finally(() => { + if (pathProbeInFlight.get(cacheKey) === request) { + pathProbeInFlight.delete(cacheKey); + } + }); + pathProbeInFlight.set(cacheKey, request); + return request; +} + async function resolveVerifiedPathOpenCodeBinaryPath( options: OpenCodeRuntimeBinaryResolveOptions = {} ): Promise { - const result = await probeFirstWorkingPathOpenCodeBinary(options); + const result = await probeFirstWorkingPathOpenCodeBinaryCached(options); return result.ok ? result.binaryPath : null; } +export function clearOpenCodeRuntimeBinaryResolverCache(): void { + versionProbeCache.clear(); + versionProbeInFlight.clear(); + pathProbeCache.clear(); + pathProbeInFlight.clear(); + runtimeBinaryResolveCache.clear(); + runtimeBinaryResolveInFlight.clear(); +} + export async function resolveVerifiedOpenCodeRuntimeBinaryPath( options: OpenCodeRuntimeBinaryResolveOptions = {} ): Promise { - return ( + const cacheKey = getPathProbeCacheKey(options); + const cached = runtimeBinaryResolveCache.get(cacheKey); + if (cached && Date.now() - cached.cachedAt < cached.ttlMs) { + return cached.binaryPath; + } + + const inFlight = runtimeBinaryResolveInFlight.get(cacheKey); + if (inFlight) { + return inFlight; + } + + const request = (async () => (await resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()) ?? - (await resolveVerifiedPathOpenCodeBinaryPath(options)) - ); + (await resolveVerifiedPathOpenCodeBinaryPath(options)))() + .then((binaryPath) => { + runtimeBinaryResolveCache.set(cacheKey, { + binaryPath, + cachedAt: Date.now(), + ttlMs: getRuntimeBinaryResolveTtlMs(binaryPath), + }); + return binaryPath; + }) + .finally(() => { + if (runtimeBinaryResolveInFlight.get(cacheKey) === request) { + runtimeBinaryResolveInFlight.delete(cacheKey); + } + }); + runtimeBinaryResolveInFlight.set(cacheKey, request); + return request; } function isLinuxMuslRuntime(): boolean { @@ -511,6 +651,8 @@ export class OpenCodeRuntimeInstallerService { private mainWindow: BrowserWindow | null = null; private installPromise: Promise | null = null; private latestStatus: OpenCodeRuntimeStatus | null = null; + private latestStatusAt = 0; + private statusPromise: Promise | null = null; setMainWindow(win: BrowserWindow | null): void { this.mainWindow = win; @@ -518,16 +660,40 @@ export class OpenCodeRuntimeInstallerService { invalidateStatusCache(): void { this.latestStatus = null; + this.latestStatusAt = 0; + this.statusPromise = null; + clearOpenCodeRuntimeBinaryResolverCache(); } async getStatus(): Promise { if (this.installPromise && this.latestStatus) { return this.latestStatus; } + if (this.installPromise) { + return this.installPromise; + } + if (this.latestStatus && Date.now() - this.latestStatusAt < this.getStatusCacheTtlMs()) { + return this.latestStatus; + } + + if (this.statusPromise) { + return this.statusPromise; + } + + const request = this.resolveStatus().finally(() => { + if (this.statusPromise === request) { + this.statusPromise = null; + } + }); + this.statusPromise = request; + return request; + } + + private async resolveStatus(): Promise { const appManagedStatus = await this.getAppManagedStatus(); if (appManagedStatus.installed) { - this.latestStatus = appManagedStatus; + this.rememberStatus(appManagedStatus); return appManagedStatus; } @@ -538,7 +704,7 @@ export class OpenCodeRuntimeInstallerService { appManagedStatus.state !== 'failed' ? pathStatus : appManagedStatus; - this.latestStatus = status; + this.rememberStatus(status); return status; } @@ -553,10 +719,21 @@ export class OpenCodeRuntimeInstallerService { } private publish(status: OpenCodeRuntimeStatus): void { - this.latestStatus = status; + this.rememberStatus(status); safeSendToRenderer(this.mainWindow, CHANNEL, status); } + private rememberStatus(status: OpenCodeRuntimeStatus): void { + this.latestStatus = status; + this.latestStatusAt = Date.now(); + } + + private getStatusCacheTtlMs(): number { + return this.latestStatus?.installed === true + ? RUNTIME_STATUS_SUCCESS_CACHE_TTL_MS + : RUNTIME_STATUS_FAILURE_CACHE_TTL_MS; + } + private publishProgress(progress: OpenCodeRuntimeInstallProgress): void { this.publish({ installed: false, @@ -571,32 +748,28 @@ export class OpenCodeRuntimeInstallerService { if (!isAbsoluteExistingFile(manifest?.binaryPath)) { return { installed: false, source: 'missing', state: 'idle' }; } - try { - const { stdout } = await execCli(manifest.binaryPath, ['--version'], { - timeout: VERSION_TIMEOUT_MS, - windowsHide: true, - }); + const version = await probeOpenCodeBinaryVersionCached(manifest.binaryPath); + if (version.ok) { return { installed: true, binaryPath: manifest.binaryPath, - version: stdout.trim() || manifest.version, + version: version.version ?? manifest.version, source: 'app-managed', state: 'ready', }; - } catch (error) { - return { - installed: false, - binaryPath: manifest.binaryPath, - version: manifest.version, - source: 'app-managed', - state: 'failed', - error: getErrorMessage(error), - }; } + return { + installed: false, + binaryPath: manifest.binaryPath, + version: manifest.version, + source: 'app-managed', + state: 'failed', + error: version.error, + }; } private async getPathStatus(): Promise { - const result = await probeFirstWorkingPathOpenCodeBinary(); + const result = await probeFirstWorkingPathOpenCodeBinaryCached(); if (result.ok) { return { installed: true, @@ -687,6 +860,7 @@ export class OpenCodeRuntimeInstallerService { `${JSON.stringify(manifest, null, 2)}\n`, 'utf8' ); + clearOpenCodeRuntimeBinaryResolverCache(); const status: OpenCodeRuntimeStatus = { installed: true, diff --git a/test/main/services/infrastructure/OpenCodeRuntimeInstallerService.test.ts b/test/main/services/infrastructure/OpenCodeRuntimeInstallerService.test.ts index 563d6e49..4a93dbba 100644 --- a/test/main/services/infrastructure/OpenCodeRuntimeInstallerService.test.ts +++ b/test/main/services/infrastructure/OpenCodeRuntimeInstallerService.test.ts @@ -28,6 +28,7 @@ vi.mock('@main/utils/shellEnv', () => ({ })); import { + clearOpenCodeRuntimeBinaryResolverCache, extractOpenCodeRuntimeBinaryFromTarball, getOpenCodeRuntimePlatformCandidates, OpenCodeRuntimeInstallerService, @@ -41,6 +42,20 @@ import { setAppDataBasePath } from '@main/utils/pathDecoder'; let tempRoot: string | null = null; let originalPath: string | undefined; +function deferred(): { + promise: Promise; + resolve: (value: T) => void; + reject: (error: unknown) => void; +} { + let resolve!: (value: T) => void; + let reject!: (error: unknown) => void; + const promise = new Promise((promiseResolve, promiseReject) => { + resolve = promiseResolve; + reject = promiseReject; + }); + return { promise, resolve, reject }; +} + function writeOctal(header: Buffer, offset: number, length: number, value: number): void { const encoded = value .toString(8) @@ -84,6 +99,7 @@ describe('OpenCodeRuntimeInstallerService resolver', () => { setAppDataBasePath(tempRoot); originalPath = process.env.PATH; process.env.PATH = ''; + clearOpenCodeRuntimeBinaryResolverCache(); execCliMock.mockReset(); execCliMock.mockResolvedValue({ stdout: 'opencode 1.0.0\n', stderr: '' }); buildMergedCliPathMock.mockReset(); @@ -97,6 +113,7 @@ describe('OpenCodeRuntimeInstallerService resolver', () => { }); afterEach(async () => { + clearOpenCodeRuntimeBinaryResolverCache(); setAppDataBasePath(null); if (originalPath === undefined) { delete process.env.PATH; @@ -194,11 +211,53 @@ describe('OpenCodeRuntimeInstallerService resolver', () => { windowsHide: true, }); + clearOpenCodeRuntimeBinaryResolverCache(); execCliMock.mockRejectedValueOnce(new Error('broken binary')); await expect(resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()).resolves.toBeNull(); }); + it('coalesces concurrent app-managed OpenCode verification probes', async () => { + const binaryPath = path.join( + tempRoot!, + 'data', + 'runtimes', + 'opencode', + 'versions', + '1.0.0', + 'opencode-test', + 'opencode' + ); + const manifestPath = path.join(tempRoot!, 'data', 'runtimes', 'opencode', 'current.json'); + await mkdir(path.dirname(binaryPath), { recursive: true }); + await mkdir(path.dirname(manifestPath), { recursive: true }); + await writeFile(binaryPath, 'binary', { mode: 0o755 }); + await writeFile( + manifestPath, + `${JSON.stringify({ + schemaVersion: 1, + version: '1.0.0', + platformPackage: 'opencode-test', + binaryPath, + integrity: 'sha512-test', + installedAt: '2026-05-12T00:00:00.000Z', + })}\n`, + 'utf8' + ); + const versionProbe = deferred<{ stdout: string; stderr: string }>(); + execCliMock.mockReturnValue(versionProbe.promise); + + const first = resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath(); + const second = resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath(); + await vi.waitFor(() => expect(execCliMock).toHaveBeenCalledTimes(1)); + + versionProbe.resolve({ stdout: 'opencode 1.0.0\n', stderr: '' }); + await expect(Promise.all([first, second])).resolves.toEqual([binaryPath, binaryPath]); + + await expect(resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()).resolves.toBe(binaryPath); + expect(execCliMock).toHaveBeenCalledTimes(1); + }); + it('returns a verified OpenCode binary from best-effort shell PATH when app-managed runtime is absent', async () => { const binaryPath = path.join(tempRoot!, 'homebrew', 'bin', 'opencode'); await mkdir(path.dirname(binaryPath), { recursive: true }); @@ -222,6 +281,61 @@ describe('OpenCodeRuntimeInstallerService resolver', () => { }); }); + it('coalesces concurrent verified OpenCode PATH probes and reuses the warm result', async () => { + const binaryPath = path.join(tempRoot!, 'homebrew', 'bin', 'opencode'); + await mkdir(path.dirname(binaryPath), { recursive: true }); + await writeFile(binaryPath, 'binary', { mode: 0o755 }); + resolveInteractiveShellEnvBestEffortMock.mockResolvedValue({ + PATH: path.dirname(binaryPath), + }); + getShellPreferredHomeMock.mockReturnValue(tempRoot!); + const versionProbe = deferred<{ stdout: string; stderr: string }>(); + execCliMock.mockReturnValue(versionProbe.promise); + + const first = resolveVerifiedOpenCodeRuntimeBinaryPath({ shellEnvTimeoutMs: 0 }); + const second = resolveVerifiedOpenCodeRuntimeBinaryPath({ shellEnvTimeoutMs: 0 }); + await vi.waitFor(() => expect(execCliMock).toHaveBeenCalledTimes(1)); + + versionProbe.resolve({ stdout: 'opencode 1.0.0\n', stderr: '' }); + await expect(Promise.all([first, second])).resolves.toEqual([binaryPath, binaryPath]); + + await expect(resolveVerifiedOpenCodeRuntimeBinaryPath({ shellEnvTimeoutMs: 0 })).resolves.toBe( + binaryPath + ); + expect(execCliMock).toHaveBeenCalledTimes(1); + expect(resolveInteractiveShellEnvBestEffortMock).toHaveBeenCalledTimes(1); + }); + + it('coalesces concurrent OpenCode runtime status checks and serves a short warm cache', async () => { + const binaryPath = path.join(tempRoot!, 'homebrew', 'bin', 'opencode'); + await mkdir(path.dirname(binaryPath), { recursive: true }); + await writeFile(binaryPath, 'binary', { mode: 0o755 }); + resolveInteractiveShellEnvBestEffortMock.mockResolvedValue({ + PATH: path.dirname(binaryPath), + }); + getShellPreferredHomeMock.mockReturnValue(tempRoot!); + const versionProbe = deferred<{ stdout: string; stderr: string }>(); + execCliMock.mockReturnValue(versionProbe.promise); + const service = new OpenCodeRuntimeInstallerService(); + + const first = service.getStatus(); + const second = service.getStatus(); + await vi.waitFor(() => expect(execCliMock).toHaveBeenCalledTimes(1)); + + versionProbe.resolve({ stdout: 'opencode 1.0.0\n', stderr: '' }); + await expect(Promise.all([first, second])).resolves.toMatchObject([ + { installed: true, source: 'path', binaryPath }, + { installed: true, source: 'path', binaryPath }, + ]); + + await expect(service.getStatus()).resolves.toMatchObject({ + installed: true, + source: 'path', + binaryPath, + }); + expect(execCliMock).toHaveBeenCalledTimes(1); + }); + it('returns a verified OpenCode binary from the merged CLI PATH after zero-wait shell fallback', async () => { const binaryPath = path.join(tempRoot!, 'merged-cli-path', 'bin', 'opencode'); await mkdir(path.dirname(binaryPath), { recursive: true }); From 4640e1eea4f81df63cab368d8802b875243041af Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 10:39:50 +0300 Subject: [PATCH 03/59] fix(startup): ignore stale opencode probe results --- .../OpenCodeRuntimeInstallerService.ts | 62 +++++++++++++------ .../OpenCodeRuntimeInstallerService.test.ts | 60 ++++++++++++++++++ 2 files changed, 103 insertions(+), 19 deletions(-) diff --git a/src/main/services/infrastructure/OpenCodeRuntimeInstallerService.ts b/src/main/services/infrastructure/OpenCodeRuntimeInstallerService.ts index 7d2bbf49..811e4356 100644 --- a/src/main/services/infrastructure/OpenCodeRuntimeInstallerService.ts +++ b/src/main/services/infrastructure/OpenCodeRuntimeInstallerService.ts @@ -194,6 +194,7 @@ const pathProbeCache = new Map(); const pathProbeInFlight = new Map>(); const runtimeBinaryResolveCache = new Map(); const runtimeBinaryResolveInFlight = new Map>(); +let runtimeResolverCacheGeneration = 0; async function probeOpenCodeBinaryVersion(binaryPath: string): Promise { try { @@ -230,13 +231,16 @@ async function probeOpenCodeBinaryVersionCached( return inFlight; } + const cacheGeneration = runtimeResolverCacheGeneration; const request = probeOpenCodeBinaryVersion(binaryPath) .then((result) => { - versionProbeCache.set(cacheKey, { - result, - cachedAt: Date.now(), - ttlMs: getVersionProbeTtlMs(result), - }); + if (cacheGeneration === runtimeResolverCacheGeneration) { + versionProbeCache.set(cacheKey, { + result, + cachedAt: Date.now(), + ttlMs: getVersionProbeTtlMs(result), + }); + } return result; }) .finally(() => { @@ -356,13 +360,16 @@ async function probeFirstWorkingPathOpenCodeBinaryCached( return inFlight; } + const cacheGeneration = runtimeResolverCacheGeneration; const request = probeFirstWorkingPathOpenCodeBinary(options) .then((result) => { - pathProbeCache.set(cacheKey, { - result, - cachedAt: Date.now(), - ttlMs: getPathProbeTtlMs(result), - }); + if (cacheGeneration === runtimeResolverCacheGeneration) { + pathProbeCache.set(cacheKey, { + result, + cachedAt: Date.now(), + ttlMs: getPathProbeTtlMs(result), + }); + } return result; }) .finally(() => { @@ -382,6 +389,7 @@ async function resolveVerifiedPathOpenCodeBinaryPath( } export function clearOpenCodeRuntimeBinaryResolverCache(): void { + runtimeResolverCacheGeneration += 1; versionProbeCache.clear(); versionProbeInFlight.clear(); pathProbeCache.clear(); @@ -404,15 +412,18 @@ export async function resolveVerifiedOpenCodeRuntimeBinaryPath( return inFlight; } + const cacheGeneration = runtimeResolverCacheGeneration; const request = (async () => (await resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()) ?? (await resolveVerifiedPathOpenCodeBinaryPath(options)))() .then((binaryPath) => { - runtimeBinaryResolveCache.set(cacheKey, { - binaryPath, - cachedAt: Date.now(), - ttlMs: getRuntimeBinaryResolveTtlMs(binaryPath), - }); + if (cacheGeneration === runtimeResolverCacheGeneration) { + runtimeBinaryResolveCache.set(cacheKey, { + binaryPath, + cachedAt: Date.now(), + ttlMs: getRuntimeBinaryResolveTtlMs(binaryPath), + }); + } return binaryPath; }) .finally(() => { @@ -653,12 +664,14 @@ export class OpenCodeRuntimeInstallerService { private latestStatus: OpenCodeRuntimeStatus | null = null; private latestStatusAt = 0; private statusPromise: Promise | null = null; + private statusCacheGeneration = 0; setMainWindow(win: BrowserWindow | null): void { this.mainWindow = win; } invalidateStatusCache(): void { + this.statusCacheGeneration += 1; this.latestStatus = null; this.latestStatusAt = 0; this.statusPromise = null; @@ -681,7 +694,8 @@ export class OpenCodeRuntimeInstallerService { return this.statusPromise; } - const request = this.resolveStatus().finally(() => { + const statusCacheGeneration = this.statusCacheGeneration; + const request = this.resolveStatus(statusCacheGeneration).finally(() => { if (this.statusPromise === request) { this.statusPromise = null; } @@ -690,10 +704,10 @@ export class OpenCodeRuntimeInstallerService { return request; } - private async resolveStatus(): Promise { + private async resolveStatus(statusCacheGeneration: number): Promise { const appManagedStatus = await this.getAppManagedStatus(); if (appManagedStatus.installed) { - this.rememberStatus(appManagedStatus); + this.rememberStatusIfCurrent(appManagedStatus, statusCacheGeneration); return appManagedStatus; } @@ -704,7 +718,7 @@ export class OpenCodeRuntimeInstallerService { appManagedStatus.state !== 'failed' ? pathStatus : appManagedStatus; - this.rememberStatus(status); + this.rememberStatusIfCurrent(status, statusCacheGeneration); return status; } @@ -719,10 +733,20 @@ export class OpenCodeRuntimeInstallerService { } private publish(status: OpenCodeRuntimeStatus): void { + this.statusCacheGeneration += 1; this.rememberStatus(status); safeSendToRenderer(this.mainWindow, CHANNEL, status); } + private rememberStatusIfCurrent( + status: OpenCodeRuntimeStatus, + statusCacheGeneration: number + ): void { + if (statusCacheGeneration === this.statusCacheGeneration) { + this.rememberStatus(status); + } + } + private rememberStatus(status: OpenCodeRuntimeStatus): void { this.latestStatus = status; this.latestStatusAt = Date.now(); diff --git a/test/main/services/infrastructure/OpenCodeRuntimeInstallerService.test.ts b/test/main/services/infrastructure/OpenCodeRuntimeInstallerService.test.ts index 4a93dbba..114790c6 100644 --- a/test/main/services/infrastructure/OpenCodeRuntimeInstallerService.test.ts +++ b/test/main/services/infrastructure/OpenCodeRuntimeInstallerService.test.ts @@ -306,6 +306,30 @@ describe('OpenCodeRuntimeInstallerService resolver', () => { expect(resolveInteractiveShellEnvBestEffortMock).toHaveBeenCalledTimes(1); }); + it('does not warm verified OpenCode PATH caches from a stale in-flight probe', async () => { + const binaryPath = path.join(tempRoot!, 'homebrew', 'bin', 'opencode'); + await mkdir(path.dirname(binaryPath), { recursive: true }); + await writeFile(binaryPath, 'binary', { mode: 0o755 }); + resolveInteractiveShellEnvBestEffortMock.mockResolvedValue({ + PATH: path.dirname(binaryPath), + }); + getShellPreferredHomeMock.mockReturnValue(tempRoot!); + const versionProbe = deferred<{ stdout: string; stderr: string }>(); + execCliMock.mockReturnValueOnce(versionProbe.promise); + + const staleResolve = resolveVerifiedOpenCodeRuntimeBinaryPath({ shellEnvTimeoutMs: 0 }); + await vi.waitFor(() => expect(execCliMock).toHaveBeenCalledTimes(1)); + + clearOpenCodeRuntimeBinaryResolverCache(); + versionProbe.resolve({ stdout: 'opencode 1.0.0\n', stderr: '' }); + await expect(staleResolve).resolves.toBe(binaryPath); + + await expect(resolveVerifiedOpenCodeRuntimeBinaryPath({ shellEnvTimeoutMs: 0 })).resolves.toBe( + binaryPath + ); + expect(execCliMock).toHaveBeenCalledTimes(2); + }); + it('coalesces concurrent OpenCode runtime status checks and serves a short warm cache', async () => { const binaryPath = path.join(tempRoot!, 'homebrew', 'bin', 'opencode'); await mkdir(path.dirname(binaryPath), { recursive: true }); @@ -336,6 +360,42 @@ describe('OpenCodeRuntimeInstallerService resolver', () => { expect(execCliMock).toHaveBeenCalledTimes(1); }); + it('does not remember OpenCode runtime status from a stale in-flight check', async () => { + const binaryPath = path.join(tempRoot!, 'homebrew', 'bin', 'opencode'); + await mkdir(path.dirname(binaryPath), { recursive: true }); + await writeFile(binaryPath, 'binary', { mode: 0o755 }); + resolveInteractiveShellEnvBestEffortMock.mockResolvedValue({ + PATH: path.dirname(binaryPath), + }); + getShellPreferredHomeMock.mockReturnValue(tempRoot!); + const versionProbe = deferred<{ stdout: string; stderr: string }>(); + execCliMock.mockReturnValueOnce(versionProbe.promise).mockResolvedValue({ + stdout: 'opencode 2.0.0\n', + stderr: '', + }); + const service = new OpenCodeRuntimeInstallerService(); + + const staleStatus = service.getStatus(); + await vi.waitFor(() => expect(execCliMock).toHaveBeenCalledTimes(1)); + + service.invalidateStatusCache(); + versionProbe.resolve({ stdout: 'opencode 1.0.0\n', stderr: '' }); + await expect(staleStatus).resolves.toMatchObject({ + installed: true, + source: 'path', + binaryPath, + version: 'opencode 1.0.0', + }); + + await expect(service.getStatus()).resolves.toMatchObject({ + installed: true, + source: 'path', + binaryPath, + version: 'opencode 2.0.0', + }); + expect(execCliMock).toHaveBeenCalledTimes(2); + }); + it('returns a verified OpenCode binary from the merged CLI PATH after zero-wait shell fallback', async () => { const binaryPath = path.join(tempRoot!, 'merged-cli-path', 'bin', 'opencode'); await mkdir(path.dirname(binaryPath), { recursive: true }); From c2bc20bebd2f6e789e09f268cae4b131da8348bf Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 13:24:49 +0300 Subject: [PATCH 04/59] perf(recent-projects): bound codex session discovery --- .../input/http/registerRecentProjectsHttp.ts | 42 ++- .../input/ipc/registerRecentProjectsIpc.ts | 42 ++- ...xSessionFileRecentProjectsSourceAdapter.ts | 244 +++++++++++++++--- ...ionFileRecentProjectsSourceAdapter.test.ts | 99 +++++++ 4 files changed, 382 insertions(+), 45 deletions(-) diff --git a/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts b/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts index 104ccb1a..afac9c48 100644 --- a/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +++ b/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts @@ -10,18 +10,48 @@ import type { FastifyInstance } from 'fastify'; const logger = createLogger('Feature:RecentProjects:HTTP'); +function getPayloadBytes(value: unknown): number { + try { + return Buffer.byteLength(JSON.stringify(value), 'utf8'); + } catch { + return -1; + } +} + +function getMemoryDiagnostics(): { + rssBytes: number; + heapUsedBytes: number; + heapTotalBytes: number; +} { + const memory = process.memoryUsage(); + return { + rssBytes: memory.rss, + heapUsedBytes: memory.heapUsed, + heapTotalBytes: memory.heapTotal, + }; +} + export function registerRecentProjectsHttp( app: FastifyInstance, feature: RecentProjectsFeatureFacade ): void { app.get(DASHBOARD_RECENT_PROJECTS_ROUTE, async (): Promise => { + const startedAt = Date.now(); try { - return ( - normalizeDashboardRecentProjectsPayload(await feature.listDashboardRecentProjects()) ?? { - projects: [], - degraded: true, - } - ); + const payload = normalizeDashboardRecentProjectsPayload( + await feature.listDashboardRecentProjects() + ) ?? { + projects: [], + degraded: true, + }; + logger.info('dashboard recent-projects HTTP loaded', { + count: payload.projects.length, + degraded: payload.degraded, + durationMs: Date.now() - startedAt, + payloadBytes: getPayloadBytes(payload), + ...getMemoryDiagnostics(), + }); + return payload; } catch (error) { logger.error('Failed to load dashboard recent projects via HTTP', error); return { projects: [], degraded: true }; diff --git a/src/features/recent-projects/main/adapters/input/ipc/registerRecentProjectsIpc.ts b/src/features/recent-projects/main/adapters/input/ipc/registerRecentProjectsIpc.ts index a18ea436..00639cc5 100644 --- a/src/features/recent-projects/main/adapters/input/ipc/registerRecentProjectsIpc.ts +++ b/src/features/recent-projects/main/adapters/input/ipc/registerRecentProjectsIpc.ts @@ -9,18 +9,48 @@ import type { IpcMain } from 'electron'; const logger = createLogger('Feature:RecentProjects:IPC'); +function getPayloadBytes(value: unknown): number { + try { + return Buffer.byteLength(JSON.stringify(value), 'utf8'); + } catch { + return -1; + } +} + +function getMemoryDiagnostics(): { + rssBytes: number; + heapUsedBytes: number; + heapTotalBytes: number; +} { + const memory = process.memoryUsage(); + return { + rssBytes: memory.rss, + heapUsedBytes: memory.heapUsed, + heapTotalBytes: memory.heapTotal, + }; +} + export function registerRecentProjectsIpc( ipcMain: IpcMain, feature: RecentProjectsFeatureFacade ): void { ipcMain.handle(GET_DASHBOARD_RECENT_PROJECTS, async () => { + const startedAt = Date.now(); try { - return ( - normalizeDashboardRecentProjectsPayload(await feature.listDashboardRecentProjects()) ?? { - projects: [], - degraded: true, - } - ); + const payload = normalizeDashboardRecentProjectsPayload( + await feature.listDashboardRecentProjects() + ) ?? { + projects: [], + degraded: true, + }; + logger.info('dashboard recent-projects IPC loaded', { + count: payload.projects.length, + degraded: payload.degraded, + durationMs: Date.now() - startedAt, + payloadBytes: getPayloadBytes(payload), + ...getMemoryDiagnostics(), + }); + return payload; } catch (error) { logger.error('Failed to load dashboard recent projects via IPC', error); return { projects: [], degraded: true }; diff --git a/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts b/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts index 2258fc39..cb7c54bc 100644 --- a/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts +++ b/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts @@ -23,6 +23,7 @@ const CODEX_SESSION_FILE_SOFT_BUDGET_MS = 6_500; const CODEX_SESSION_FILE_MAX_UNCACHED_READS_PER_RUN = 160; const CODEX_SESSION_FILE_READ_BATCH_SIZE = 24; const CODEX_SESSION_FILE_READ_TIMEOUT_MS = 700; +const CODEX_SESSION_FILE_DISCOVERY_STAT_BATCH_SIZE = 64; const CODEX_SESSION_METADATA_READ_LIMIT_BYTES = 128 * 1024; const CODEX_SESSION_FILE_CACHE_SCHEMA_VERSION = 1; const CODEX_SESSION_FILE_CACHE_RELATIVE_PATH = path.join( @@ -80,6 +81,11 @@ interface CodexSessionSnapshotLoadResult { degraded: boolean; stats: { files: number; + visitedFiles: number; + droppedOlderFiles: number; + statFailures: number; + directoriesVisited: number; + discoveryTimedOut: boolean; cached: number; uncachedReads: number; timedOutReads: number; @@ -88,6 +94,14 @@ interface CodexSessionSnapshotLoadResult { }; } +interface CodexSessionFileListingResult { + files: CodexSessionFileEntry[]; + visitedFiles: number; + statFailures: number; + directoriesVisited: number; + timedOut: boolean; +} + function emptyCache(): CodexSessionFileCacheFile { return { schemaVersion: CODEX_SESSION_FILE_CACHE_SCHEMA_VERSION, @@ -95,6 +109,21 @@ function emptyCache(): CodexSessionFileCacheFile { }; } +function captureMemoryDiagnostics(): { + rssBytes: number; + heapUsedBytes: number; + heapTotalBytes: number; + externalBytes: number; +} { + const memory = process.memoryUsage(); + return { + rssBytes: memory.rss, + heapUsedBytes: memory.heapUsed, + heapTotalBytes: memory.heapTotal, + externalBytes: memory.external, + }; +} + function isUsableCacheEntry( entry: CodexSessionFileCacheEntry | undefined, file: CodexSessionFileEntry @@ -211,45 +240,149 @@ async function readFirstLineWithTimeout( return result; } -async function listJsonlFiles(root: string, maxDepth: number): Promise { - async function walk(directory: string, depth: number): Promise { +function insertRecentSessionFile( + files: CodexSessionFileEntry[], + file: CodexSessionFileEntry, + limit: number +): void { + if (limit <= 0) { + return; + } + + if (files.length >= limit && file.mtimeMs <= files[files.length - 1].mtimeMs) { + return; + } + + let low = 0; + let high = files.length; + while (low < high) { + const mid = Math.floor((low + high) / 2); + if (file.mtimeMs > files[mid].mtimeMs) { + high = mid; + } else { + low = mid + 1; + } + } + + files.splice(low, 0, file); + if (files.length > limit) { + files.pop(); + } +} + +function selectMostRecentSessionFiles( + files: CodexSessionFileEntry[], + limit: number +): CodexSessionFileEntry[] { + const selected: CodexSessionFileEntry[] = []; + for (const file of files) { + insertRecentSessionFile(selected, file, limit); + } + return selected; +} + +async function listRecentJsonlFiles( + root: string, + maxDepth: number, + limit: number, + deadlineMs: number +): Promise { + const selectedFiles: CodexSessionFileEntry[] = []; + let visitedFiles = 0; + let statFailures = 0; + let directoriesVisited = 0; + let timedOut = false; + + const hasBudget = (): boolean => { + if (Date.now() < deadlineMs) { + return true; + } + timedOut = true; + return false; + }; + + async function statJsonlFile(filePath: string): Promise { + if (!hasBudget()) { + return null; + } + visitedFiles += 1; + try { + const stats = await fs.stat(filePath); + return { + filePath, + mtimeMs: stats.mtimeMs, + size: stats.size, + }; + } catch { + statFailures += 1; + return null; + } + } + + async function collectFileStats(filePaths: string[]): Promise { + for ( + let offset = 0; + offset < filePaths.length && hasBudget(); + offset += CODEX_SESSION_FILE_DISCOVERY_STAT_BATCH_SIZE + ) { + const batch = filePaths.slice(offset, offset + CODEX_SESSION_FILE_DISCOVERY_STAT_BATCH_SIZE); + const stats = await Promise.all(batch.map((filePath) => statJsonlFile(filePath))); + for (const file of stats) { + if (file) { + insertRecentSessionFile(selectedFiles, file, limit); + } + } + } + } + + async function walk(directory: string, depth: number): Promise { + if (!hasBudget()) { + return; + } let entries; try { entries = await fs.readdir(directory, { withFileTypes: true, encoding: 'utf8' }); } catch { - return []; + return; } - const files = await Promise.all( - entries.map(async (entry): Promise => { - const entryPath = path.join(directory, entry.name); - if (entry.isDirectory()) { - return depth < maxDepth ? walk(entryPath, depth + 1) : []; - } + directoriesVisited += 1; + const filePaths: string[] = []; + const childDirectories: string[] = []; - if (!entry.isFile() || !entry.name.endsWith('.jsonl')) { - return []; + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + if (depth < maxDepth) { + childDirectories.push(entryPath); } + continue; + } - try { - const stats = await fs.stat(entryPath); - return [ - { - filePath: entryPath, - mtimeMs: stats.mtimeMs, - size: stats.size, - }, - ]; - } catch { - return []; - } - }) - ); + if (entry.isFile() && entry.name.endsWith('.jsonl')) { + filePaths.push(entryPath); + } + } - return files.flat(); + await collectFileStats(filePaths); + + for (const childDirectory of childDirectories) { + if (!hasBudget()) { + return; + } + await walk(childDirectory, depth + 1); + } } - return walk(root, 0); + await walk(root, 0); + + return { + files: selectedFiles, + visitedFiles, + statFailures, + directoriesVisited, + timedOut, + }; } function parseSessionSnapshot( @@ -294,6 +427,7 @@ export class CodexSessionFileRecentProjectsSourceAdapter implements RecentProjec readonly timeoutMs = CODEX_SESSION_FILE_SOURCE_TIMEOUT_MS; readonly #codexHome: string; readonly #cachePath: string; + #inFlightList: Promise | null = null; constructor( private readonly deps: { @@ -313,6 +447,20 @@ export class CodexSessionFileRecentProjectsSourceAdapter implements RecentProjec } async list(): Promise { + if (this.#inFlightList) { + return this.#inFlightList; + } + + const request = this.#listUncached().finally(() => { + if (this.#inFlightList === request) { + this.#inFlightList = null; + } + }); + this.#inFlightList = request; + return request; + } + + async #listUncached(): Promise { const activeContext = this.deps.getActiveContext(); const localContext = this.deps.getLocalContext(); @@ -339,6 +487,7 @@ export class CodexSessionFileRecentProjectsSourceAdapter implements RecentProjec count: validCandidates.length, codexHome: this.#codexHome, degraded: snapshotResult.degraded, + ...captureMemoryDiagnostics(), ...snapshotResult.stats, }); @@ -361,16 +510,34 @@ export class CodexSessionFileRecentProjectsSourceAdapter implements RecentProjec async #listRecentSessionSnapshots(): Promise { const startedAt = Date.now(); const deadline = startedAt + CODEX_SESSION_FILE_SOFT_BUDGET_MS; - const files = [ - ...(await listJsonlFiles(path.join(this.#codexHome, 'sessions'), 4)), - ...(await listJsonlFiles(path.join(this.#codexHome, 'archived_sessions'), 1)), - ].sort((left, right) => right.mtimeMs - left.mtimeMs); + const sessionFiles = await listRecentJsonlFiles( + path.join(this.#codexHome, 'sessions'), + 4, + CODEX_SESSION_FILE_PARSE_LIMIT, + deadline + ); + const archivedSessionFiles = await listRecentJsonlFiles( + path.join(this.#codexHome, 'archived_sessions'), + 1, + CODEX_SESSION_FILE_PARSE_LIMIT, + deadline + ); + const files = selectMostRecentSessionFiles( + [...sessionFiles.files, ...archivedSessionFiles.files], + CODEX_SESSION_FILE_PARSE_LIMIT + ); + const visitedFiles = sessionFiles.visitedFiles + archivedSessionFiles.visitedFiles; + const statFailures = sessionFiles.statFailures + archivedSessionFiles.statFailures; + const directoriesVisited = + sessionFiles.directoriesVisited + archivedSessionFiles.directoriesVisited; + const droppedOlderFiles = Math.max(0, visitedFiles - statFailures - files.length); + const discoveryTimedOut = sessionFiles.timedOut || archivedSessionFiles.timedOut; const snapshotsByCwd = new Map(); - const candidateFiles = files.slice(0, CODEX_SESSION_FILE_PARSE_LIMIT); + const candidateFiles = files; const cache = await this.#readCacheSafe(); const nextCacheEntries = new Map(); - let degraded = false; + let degraded = discoveryTimedOut; let cached = 0; let uncachedReads = 0; let timedOutReads = 0; @@ -454,6 +621,12 @@ export class CodexSessionFileRecentProjectsSourceAdapter implements RecentProjec if (degraded) { this.deps.logger.warn('codex session-file recent-projects source partial', { files: candidateFiles.length, + visitedFiles, + droppedOlderFiles, + statFailures, + directoriesVisited, + discoveryTimedOut, + ...captureMemoryDiagnostics(), cached, uncachedReads, timedOutReads, @@ -468,6 +641,11 @@ export class CodexSessionFileRecentProjectsSourceAdapter implements RecentProjec degraded, stats: { files: candidateFiles.length, + visitedFiles, + droppedOlderFiles, + statFailures, + directoriesVisited, + discoveryTimedOut, cached, uncachedReads, timedOutReads, diff --git a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts index 691edafe..5c31443e 100644 --- a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts +++ b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts @@ -57,6 +57,20 @@ async function writeRollout( await fs.utimes(filePath, mtime, mtime); } +function deferred(): { + promise: Promise; + resolve: (value: T) => void; + reject: (error: unknown) => void; +} { + let resolve!: (value: T) => void; + let reject!: (error: unknown) => void; + const promise = new Promise((promiseResolve, promiseReject) => { + resolve = promiseResolve; + reject = promiseReject; + }); + return { promise, resolve, reject }; +} + describe('CodexSessionFileRecentProjectsSourceAdapter', () => { let tempDir: string; @@ -338,6 +352,43 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => { expect(openSpy).not.toHaveBeenCalled(); }); + it('coalesces concurrent Codex session-file source reads', async () => { + const codexHome = path.join(tempDir, '.codex'); + const logger = createLogger(); + const resolveResult = deferred(); + const identityResolver = { + resolve: vi.fn().mockReturnValue(resolveResult.promise), + } as unknown as RecentProjectIdentityResolver; + await writeRollout( + path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-alpha.jsonl'), + { + cwd: '/Users/test/projects/alpha', + branch: 'main', + }, + new Date('2026-04-14T12:00:00.000Z') + ); + + const adapter = new CodexSessionFileRecentProjectsSourceAdapter({ + getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never, + getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never, + identityResolver, + logger, + codexHome, + appDataPath: path.join(tempDir, 'app-data'), + }); + + const first = adapter.list(); + await vi.waitFor(() => expect(identityResolver.resolve).toHaveBeenCalledTimes(1)); + const second = adapter.list(); + + resolveResult.resolve(null); + await expect(Promise.all([first, second])).resolves.toEqual([ + expect.objectContaining({ degraded: false }), + expect.objectContaining({ degraded: false }), + ]); + expect(identityResolver.resolve).toHaveBeenCalledTimes(1); + }); + it('invalidates cached session metadata when the jsonl fingerprint changes', async () => { const codexHome = path.join(tempDir, '.codex'); const appDataPath = path.join(tempDir, 'app-data'); @@ -575,6 +626,54 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => { ]); }); + it('bounds discovered Codex session files before reading metadata', async () => { + const codexHome = path.join(tempDir, '.codex'); + const appDataPath = path.join(tempDir, 'app-data'); + const logger = createLogger(); + const identityResolver = { + resolve: vi.fn().mockResolvedValue(null), + } as unknown as RecentProjectIdentityResolver; + const baseTime = Date.parse('2026-04-14T12:00:00.000Z'); + + await Promise.all( + Array.from({ length: 505 }).map((_, index) => + writeRollout( + path.join(codexHome, 'sessions', '2026', '04', '14', `rollout-alpha-${index}.jsonl`), + { + cwd: '/Users/test/projects/alpha', + branch: 'main', + }, + new Date(baseTime - index * 1000) + ) + ) + ); + + const adapter = new CodexSessionFileRecentProjectsSourceAdapter({ + getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never, + getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never, + identityResolver, + logger, + codexHome, + appDataPath, + }); + const result = await adapter.list(); + + expect(result.degraded).toBe(true); + expect(result.candidates.map((candidate) => candidate.primaryPath)).toEqual([ + '/Users/test/projects/alpha', + ]); + expect(logger.warn).toHaveBeenCalledWith( + 'codex session-file recent-projects source partial', + expect.objectContaining({ + files: 500, + visitedFiles: 505, + droppedOlderFiles: 5, + uncachedReads: 160, + skippedUncached: 340, + }) + ); + }); + it('skips non-interactive and ephemeral sessions', async () => { const codexHome = path.join(tempDir, '.codex'); const logger = createLogger(); From 8c86def84dea244497e58b277899c954bb2b6446 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 13:46:33 +0300 Subject: [PATCH 05/59] fix(recent-projects): avoid stale scan diagnostics regressions --- .../input/http/registerRecentProjectsHttp.ts | 30 +++-------- .../input/ipc/registerRecentProjectsIpc.ts | 30 +++-------- .../input/recentProjectsDiagnostics.ts | 51 +++++++++++++++++++ ...xSessionFileRecentProjectsSourceAdapter.ts | 36 +++++++------ .../input/recentProjectsDiagnostics.test.ts | 49 ++++++++++++++++++ ...ionFileRecentProjectsSourceAdapter.test.ts | 40 +++++++++++++++ 6 files changed, 175 insertions(+), 61 deletions(-) create mode 100644 src/features/recent-projects/main/adapters/input/recentProjectsDiagnostics.ts create mode 100644 test/features/recent-projects/main/adapters/input/recentProjectsDiagnostics.test.ts diff --git a/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts b/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts index afac9c48..ec27002a 100644 --- a/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +++ b/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts @@ -5,32 +5,16 @@ import { } from '@features/recent-projects/contracts'; import { createLogger } from '@shared/utils/logger'; +import { + estimateDashboardRecentProjectsPayloadBytes, + getRecentProjectsMemoryDiagnostics, +} from '../recentProjectsDiagnostics'; + import type { RecentProjectsFeatureFacade } from '@features/recent-projects/main/composition/createRecentProjectsFeature'; import type { FastifyInstance } from 'fastify'; const logger = createLogger('Feature:RecentProjects:HTTP'); -function getPayloadBytes(value: unknown): number { - try { - return Buffer.byteLength(JSON.stringify(value), 'utf8'); - } catch { - return -1; - } -} - -function getMemoryDiagnostics(): { - rssBytes: number; - heapUsedBytes: number; - heapTotalBytes: number; -} { - const memory = process.memoryUsage(); - return { - rssBytes: memory.rss, - heapUsedBytes: memory.heapUsed, - heapTotalBytes: memory.heapTotal, - }; -} - export function registerRecentProjectsHttp( app: FastifyInstance, feature: RecentProjectsFeatureFacade @@ -48,8 +32,8 @@ export function registerRecentProjectsHttp( count: payload.projects.length, degraded: payload.degraded, durationMs: Date.now() - startedAt, - payloadBytes: getPayloadBytes(payload), - ...getMemoryDiagnostics(), + estimatedPayloadBytes: estimateDashboardRecentProjectsPayloadBytes(payload), + ...getRecentProjectsMemoryDiagnostics(), }); return payload; } catch (error) { diff --git a/src/features/recent-projects/main/adapters/input/ipc/registerRecentProjectsIpc.ts b/src/features/recent-projects/main/adapters/input/ipc/registerRecentProjectsIpc.ts index 00639cc5..cdfb1af7 100644 --- a/src/features/recent-projects/main/adapters/input/ipc/registerRecentProjectsIpc.ts +++ b/src/features/recent-projects/main/adapters/input/ipc/registerRecentProjectsIpc.ts @@ -4,32 +4,16 @@ import { } from '@features/recent-projects/contracts'; import { createLogger } from '@shared/utils/logger'; +import { + estimateDashboardRecentProjectsPayloadBytes, + getRecentProjectsMemoryDiagnostics, +} from '../recentProjectsDiagnostics'; + import type { RecentProjectsFeatureFacade } from '@features/recent-projects/main/composition/createRecentProjectsFeature'; import type { IpcMain } from 'electron'; const logger = createLogger('Feature:RecentProjects:IPC'); -function getPayloadBytes(value: unknown): number { - try { - return Buffer.byteLength(JSON.stringify(value), 'utf8'); - } catch { - return -1; - } -} - -function getMemoryDiagnostics(): { - rssBytes: number; - heapUsedBytes: number; - heapTotalBytes: number; -} { - const memory = process.memoryUsage(); - return { - rssBytes: memory.rss, - heapUsedBytes: memory.heapUsed, - heapTotalBytes: memory.heapTotal, - }; -} - export function registerRecentProjectsIpc( ipcMain: IpcMain, feature: RecentProjectsFeatureFacade @@ -47,8 +31,8 @@ export function registerRecentProjectsIpc( count: payload.projects.length, degraded: payload.degraded, durationMs: Date.now() - startedAt, - payloadBytes: getPayloadBytes(payload), - ...getMemoryDiagnostics(), + estimatedPayloadBytes: estimateDashboardRecentProjectsPayloadBytes(payload), + ...getRecentProjectsMemoryDiagnostics(), }); return payload; } catch (error) { diff --git a/src/features/recent-projects/main/adapters/input/recentProjectsDiagnostics.ts b/src/features/recent-projects/main/adapters/input/recentProjectsDiagnostics.ts new file mode 100644 index 00000000..f527af36 --- /dev/null +++ b/src/features/recent-projects/main/adapters/input/recentProjectsDiagnostics.ts @@ -0,0 +1,51 @@ +import type { + DashboardRecentProject, + DashboardRecentProjectsPayload, +} from '@features/recent-projects/contracts'; + +function stringBytes(value: string | undefined): number { + return value ? Buffer.byteLength(value, 'utf8') : 0; +} + +function estimateOpenTargetBytes(openTarget: DashboardRecentProject['openTarget']): number { + if (openTarget.type === 'existing-worktree') { + return stringBytes(openTarget.repositoryId) + stringBytes(openTarget.worktreeId) + 48; + } + + return stringBytes(openTarget.path) + 32; +} + +export function estimateDashboardRecentProjectsPayloadBytes( + payload: DashboardRecentProjectsPayload +): number { + let bytes = 32; + for (const project of payload.projects) { + bytes += + 160 + + stringBytes(project.id) + + stringBytes(project.name) + + stringBytes(project.primaryPath) + + stringBytes(project.primaryBranch) + + estimateOpenTargetBytes(project.openTarget); + for (const associatedPath of project.associatedPaths) { + bytes += stringBytes(associatedPath) + 8; + } + for (const providerId of project.providerIds) { + bytes += stringBytes(providerId) + 8; + } + } + return bytes; +} + +export function getRecentProjectsMemoryDiagnostics(): { + rssBytes: number; + heapUsedBytes: number; + heapTotalBytes: number; +} { + const memory = process.memoryUsage(); + return { + rssBytes: memory.rss, + heapUsedBytes: memory.heapUsed, + heapTotalBytes: memory.heapTotal, + }; +} diff --git a/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts b/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts index cb7c54bc..e2a48a87 100644 --- a/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts +++ b/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts @@ -102,6 +102,11 @@ interface CodexSessionFileListingResult { timedOut: boolean; } +interface InFlightListRequest { + contextKey: string; + promise: Promise; +} + function emptyCache(): CodexSessionFileCacheFile { return { schemaVersion: CODEX_SESSION_FILE_CACHE_SCHEMA_VERSION, @@ -427,7 +432,7 @@ export class CodexSessionFileRecentProjectsSourceAdapter implements RecentProjec readonly timeoutMs = CODEX_SESSION_FILE_SOURCE_TIMEOUT_MS; readonly #codexHome: string; readonly #cachePath: string; - #inFlightList: Promise | null = null; + #inFlightList: InFlightListRequest | null = null; constructor( private readonly deps: { @@ -447,20 +452,6 @@ export class CodexSessionFileRecentProjectsSourceAdapter implements RecentProjec } async list(): Promise { - if (this.#inFlightList) { - return this.#inFlightList; - } - - const request = this.#listUncached().finally(() => { - if (this.#inFlightList === request) { - this.#inFlightList = null; - } - }); - this.#inFlightList = request; - return request; - } - - async #listUncached(): Promise { const activeContext = this.deps.getActiveContext(); const localContext = this.deps.getLocalContext(); @@ -471,6 +462,21 @@ export class CodexSessionFileRecentProjectsSourceAdapter implements RecentProjec }; } + const contextKey = `${activeContext.type}:${activeContext.id}`; + if (this.#inFlightList?.contextKey === contextKey) { + return this.#inFlightList.promise; + } + + const request = this.#listLocal(activeContext).finally(() => { + if (this.#inFlightList?.promise === request) { + this.#inFlightList = null; + } + }); + this.#inFlightList = { contextKey, promise: request }; + return request; + } + + async #listLocal(activeContext: ServiceContext): Promise { try { const snapshotResult = await this.#listRecentSessionSnapshots(); const candidates = await Promise.all( diff --git a/test/features/recent-projects/main/adapters/input/recentProjectsDiagnostics.test.ts b/test/features/recent-projects/main/adapters/input/recentProjectsDiagnostics.test.ts new file mode 100644 index 00000000..4bc31f28 --- /dev/null +++ b/test/features/recent-projects/main/adapters/input/recentProjectsDiagnostics.test.ts @@ -0,0 +1,49 @@ +import { + estimateDashboardRecentProjectsPayloadBytes, + getRecentProjectsMemoryDiagnostics, +} from '@features/recent-projects/main/adapters/input/recentProjectsDiagnostics'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import type { DashboardRecentProjectsPayload } from '@features/recent-projects/contracts'; + +describe('recentProjectsDiagnostics', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('estimates payload size without stringifying the whole payload', () => { + const stringifySpy = vi.spyOn(JSON, 'stringify'); + const payload: DashboardRecentProjectsPayload = { + degraded: false, + projects: [ + { + id: 'repo:alpha', + name: 'alpha', + primaryPath: '/Users/test/projects/alpha', + associatedPaths: ['/Users/test/projects/alpha', '/Users/test/worktrees/alpha-feature'], + mostRecentActivity: 1_777_000_000_000, + providerIds: ['codex', 'anthropic'], + source: 'mixed', + openTarget: { + type: 'existing-worktree', + repositoryId: 'repo-alpha', + worktreeId: 'worktree-alpha-main', + }, + primaryBranch: 'main', + filesystemState: 'available', + }, + ], + }; + + expect(estimateDashboardRecentProjectsPayloadBytes(payload)).toBeGreaterThan(0); + expect(stringifySpy).not.toHaveBeenCalled(); + }); + + it('returns bounded numeric memory diagnostics', () => { + const diagnostics = getRecentProjectsMemoryDiagnostics(); + + expect(diagnostics.rssBytes).toEqual(expect.any(Number)); + expect(diagnostics.heapUsedBytes).toEqual(expect.any(Number)); + expect(diagnostics.heapTotalBytes).toEqual(expect.any(Number)); + }); +}); diff --git a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts index 5c31443e..493e49fa 100644 --- a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts +++ b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts @@ -389,6 +389,46 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => { expect(identityResolver.resolve).toHaveBeenCalledTimes(1); }); + it('does not reuse an in-flight local Codex session-file read for another active context', async () => { + const codexHome = path.join(tempDir, '.codex'); + const logger = createLogger(); + const resolveResult = deferred(); + const identityResolver = { + resolve: vi.fn().mockReturnValue(resolveResult.promise), + } as unknown as RecentProjectIdentityResolver; + let activeContext: unknown = { type: 'local', id: 'local-1' }; + await writeRollout( + path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-alpha.jsonl'), + { + cwd: '/Users/test/projects/alpha', + branch: 'main', + }, + new Date('2026-04-14T12:00:00.000Z') + ); + + const adapter = new CodexSessionFileRecentProjectsSourceAdapter({ + getActiveContext: () => activeContext as never, + getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never, + identityResolver, + logger, + codexHome, + appDataPath: path.join(tempDir, 'app-data'), + }); + + const first = adapter.list(); + await vi.waitFor(() => expect(identityResolver.resolve).toHaveBeenCalledTimes(1)); + + activeContext = { type: 'ssh', id: 'ssh-1' }; + await expect(adapter.list()).resolves.toEqual({ + candidates: [], + degraded: false, + }); + + resolveResult.resolve(null); + await expect(first).resolves.toEqual(expect.objectContaining({ degraded: false })); + expect(identityResolver.resolve).toHaveBeenCalledTimes(1); + }); + it('invalidates cached session metadata when the jsonl fingerprint changes', async () => { const codexHome = path.join(tempDir, '.codex'); const appDataPath = path.join(tempDir, 'app-data'); From 031e5eda2fdf087256faee4ab41c589fef7600a7 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 16:04:08 +0300 Subject: [PATCH 06/59] fix(recent-projects): skip oversized codex session cache --- ...xSessionFileRecentProjectsSourceAdapter.ts | 11 ++++ ...ionFileRecentProjectsSourceAdapter.test.ts | 55 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts b/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts index e2a48a87..691fcd59 100644 --- a/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts +++ b/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts @@ -30,6 +30,7 @@ const CODEX_SESSION_FILE_CACHE_RELATIVE_PATH = path.join( 'recent-projects', 'codex-session-files-index.json' ); +const CODEX_SESSION_FILE_CACHE_MAX_BYTES = 4 * 1024 * 1024; interface CodexSessionFileEntry { filePath: string; @@ -663,6 +664,16 @@ export class CodexSessionFileRecentProjectsSourceAdapter implements RecentProjec async #readCacheSafe(): Promise { try { + const stats = await fs.stat(this.#cachePath); + if (stats.size > CODEX_SESSION_FILE_CACHE_MAX_BYTES) { + this.deps.logger.warn('codex session-file recent-projects cache skipped - too large', { + cachePath: this.#cachePath, + bytes: stats.size, + maxBytes: CODEX_SESSION_FILE_CACHE_MAX_BYTES, + }); + return emptyCache(); + } + const raw = await fs.readFile(this.#cachePath, 'utf8'); const parsed = JSON.parse(raw) as Partial; if ( diff --git a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts index 493e49fa..ff4eacf7 100644 --- a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts +++ b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts @@ -596,6 +596,61 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => { }); }); + it('skips an oversized legacy session-file cache before reading it', async () => { + const codexHome = path.join(tempDir, '.codex'); + const appDataPath = path.join(tempDir, 'app-data'); + const logger = createLogger(); + const identityResolver = { + resolve: vi.fn().mockResolvedValue(null), + } as unknown as RecentProjectIdentityResolver; + await writeRollout( + path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-alpha.jsonl'), + { + cwd: '/Users/test/projects/alpha', + }, + new Date('2026-04-14T12:00:00.000Z') + ); + const cachePath = getSessionFileCachePath(appDataPath); + await fs.mkdir(path.dirname(cachePath), { recursive: true }); + await fs.writeFile(cachePath, 'x', 'utf8'); + await fs.truncate(cachePath, 4 * 1024 * 1024 + 1); + const readFileSpy = vi.spyOn(fs, 'readFile'); + + const adapter = new CodexSessionFileRecentProjectsSourceAdapter({ + getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never, + getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never, + identityResolver, + logger, + codexHome, + appDataPath, + }); + + const result = await adapter.list(); + + expect(readFileSpy).not.toHaveBeenCalledWith(cachePath, 'utf8'); + expect(result).toEqual({ + candidates: [ + expect.objectContaining({ + primaryPath: '/Users/test/projects/alpha', + }), + ], + degraded: false, + }); + expect(logger.warn).toHaveBeenCalledWith( + 'codex session-file recent-projects cache skipped - too large', + expect.objectContaining({ + bytes: 4 * 1024 * 1024 + 1, + maxBytes: 4 * 1024 * 1024, + }) + ); + await expect(fs.stat(cachePath)).resolves.toEqual( + expect.objectContaining({ + size: expect.any(Number), + }) + ); + expect((await fs.stat(cachePath)).size).toBeLessThan(4 * 1024 * 1024); + }); + it('returns a degraded partial result under the uncached read cap and completes on the next cached pass', async () => { const codexHome = path.join(tempDir, '.codex'); const appDataPath = path.join(tempDir, 'app-data'); From 72633daa6e595e1ce5fda131e301ef1917c9bb9a Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 16:08:25 +0300 Subject: [PATCH 07/59] perf(recent-projects): stream codex session discovery --- ...xSessionFileRecentProjectsSourceAdapter.ts | 44 +++++++++++++------ ...ionFileRecentProjectsSourceAdapter.test.ts | 2 + 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts b/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts index 691fcd59..3e8e7c7d 100644 --- a/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts +++ b/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts @@ -345,32 +345,50 @@ async function listRecentJsonlFiles( if (!hasBudget()) { return; } - let entries; + let directoryHandle; try { - entries = await fs.readdir(directory, { withFileTypes: true, encoding: 'utf8' }); + directoryHandle = await fs.opendir(directory, { encoding: 'utf8' }); } catch { return; } directoriesVisited += 1; - const filePaths: string[] = []; + const fileBatch: string[] = []; const childDirectories: string[] = []; + const flushFileBatch = async (): Promise => { + if (!fileBatch.length) { + return; + } + const batch = fileBatch.splice(0, fileBatch.length); + await collectFileStats(batch); + }; - for (const entry of entries) { - const entryPath = path.join(directory, entry.name); - if (entry.isDirectory()) { - if (depth < maxDepth) { - childDirectories.push(entryPath); + try { + for await (const entry of directoryHandle) { + if (!hasBudget()) { + return; } - continue; - } - if (entry.isFile() && entry.name.endsWith('.jsonl')) { - filePaths.push(entryPath); + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + if (depth < maxDepth) { + childDirectories.push(entryPath); + } + continue; + } + + if (entry.isFile() && entry.name.endsWith('.jsonl')) { + fileBatch.push(entryPath); + if (fileBatch.length >= CODEX_SESSION_FILE_DISCOVERY_STAT_BATCH_SIZE) { + await flushFileBatch(); + } + } } + } catch { + return; } - await collectFileStats(filePaths); + await flushFileBatch(); for (const childDirectory of childDirectories) { if (!hasBudget()) { diff --git a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts index ff4eacf7..3f34660e 100644 --- a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts +++ b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts @@ -742,6 +742,7 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => { ) ) ); + const readdirSpy = vi.spyOn(fs, 'readdir'); const adapter = new CodexSessionFileRecentProjectsSourceAdapter({ getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never, @@ -767,6 +768,7 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => { skippedUncached: 340, }) ); + expect(readdirSpy).not.toHaveBeenCalled(); }); it('skips non-interactive and ephemeral sessions', async () => { From b46b53d667d046224866ef250f0ec277f71b0126 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 16:35:28 +0300 Subject: [PATCH 08/59] fix(recent-projects): scope client cache by context --- .../createRecentProjectsFeature.ts | 4 +- .../hooks/useRecentProjectsSection.ts | 114 ++++++++++++------ .../utils/recentProjectsClientCache.ts | 30 +++-- .../utils/recentProjectsClientCache.test.ts | 99 ++++++++++++--- 4 files changed, 178 insertions(+), 69 deletions(-) diff --git a/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts b/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts index c85173d0..d447e8d4 100644 --- a/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +++ b/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts @@ -47,7 +47,9 @@ export function createRecentProjectsFeature(deps: { return { listDashboardRecentProjects: async () => { const activeContext = deps.getActiveContext(); - const payload = await useCase.execute(`dashboard-recent-projects:${activeContext.id}`); + const payload = await useCase.execute( + `dashboard-recent-projects:${activeContext.type}:${activeContext.id}` + ); return normalizeDashboardRecentProjectsPayload(payload) ?? { projects: [], degraded: true }; }, }; diff --git a/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts b/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts index 27f7c5d9..3eddb523 100644 --- a/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts +++ b/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts @@ -21,7 +21,6 @@ import { import { useOpenRecentProject } from './useOpenRecentProject'; import type { RecentProjectCardModel } from '../adapters/RecentProjectsSectionAdapter'; -import type { TeamSummary } from '@shared/types'; const INITIAL_RECENT_PROJECTS = 11; const LOAD_MORE_STEP = 8; @@ -70,6 +69,7 @@ export function useRecentProjectsSection( globalTasksLoading, fetchAllTasks, teams, + activeContextId, provisioningRuns, currentProvisioningRunIdByTeam, provisioningSnapshotByTeam, @@ -80,12 +80,16 @@ export function useRecentProjectsSection( globalTasksLoading: state.globalTasksLoading, fetchAllTasks: state.fetchAllTasks, teams: state.teams, + activeContextId: state.activeContextId, provisioningRuns: state.provisioningRuns, currentProvisioningRunIdByTeam: state.currentProvisioningRunIdByTeam, provisioningSnapshotByTeam: state.provisioningSnapshotByTeam, })) ); - const initialSnapshot = useMemo(() => getRecentProjectsClientSnapshot(), []); + const initialSnapshot = useMemo( + () => getRecentProjectsClientSnapshot(activeContextId), + [activeContextId] + ); const { openRecentProject, openProjectPath, selectProjectFolder } = useOpenRecentProject(); const [recentProjects, setRecentProjects] = useState( initialSnapshot?.payload.projects ?? [] @@ -105,6 +109,7 @@ export function useRecentProjectsSection( const recentProjectsRef = useRef( initialSnapshot?.payload.projects ?? [] ); + const activeContextIdRef = useRef(activeContextId); const provisioningState = useMemo( () => ({ currentProvisioningRunIdByTeam, provisioningRuns }), [currentProvisioningRunIdByTeam, provisioningRuns] @@ -125,37 +130,67 @@ export function useRecentProjectsSection( recentProjectsRef.current = recentProjects; }, [recentProjects]); - const reload = useCallback(async (options?: { force?: boolean }): Promise => { - const hasVisibleProjects = - recentProjectsRef.current.length > 0 || getRecentProjectsClientSnapshot() != null; + useEffect(() => { + activeContextIdRef.current = activeContextId; + }, [activeContextId]); - if (!hasVisibleProjects) { - setLoading(true); - } - setError(null); - try { - const payload = await loadRecentProjectsWithClientCache( - () => api.getDashboardRecentProjects(), - options - ); - setRecentProjects(payload.projects); - setRecentProjectsDegraded(payload.degraded); - setDegradedRefreshCount((current) => (payload.degraded ? current + 1 : 0)); - } catch (nextError) { - setError(nextError instanceof Error ? nextError.message : 'Failed to load recent projects'); - } finally { - setLoading(false); - } - }, []); + const reload = useCallback( + async (options?: { force?: boolean }): Promise => { + const requestContextId = activeContextId; + const hasVisibleProjects = + recentProjectsRef.current.length > 0 || + getRecentProjectsClientSnapshot(requestContextId) != null; + + if (!hasVisibleProjects) { + setLoading(true); + } + setError(null); + try { + const payload = await loadRecentProjectsWithClientCache( + requestContextId, + () => api.getDashboardRecentProjects(), + options + ); + if (activeContextIdRef.current !== requestContextId) { + return; + } + setRecentProjects(payload.projects); + setRecentProjectsDegraded(payload.degraded); + setDegradedRefreshCount((current) => (payload.degraded ? current + 1 : 0)); + } catch (nextError) { + if (activeContextIdRef.current !== requestContextId) { + return; + } + setError(nextError instanceof Error ? nextError.message : 'Failed to load recent projects'); + } finally { + if (activeContextIdRef.current === requestContextId) { + setLoading(false); + } + } + }, + [activeContextId] + ); useEffect(() => { - const snapshot = getRecentProjectsClientSnapshot(); + const snapshot = getRecentProjectsClientSnapshot(activeContextId); + if (snapshot) { + setRecentProjects(snapshot.payload.projects); + setRecentProjectsDegraded(snapshot.payload.degraded); + setDegradedRefreshCount(snapshot.payload.degraded ? 1 : 0); + setLoading(false); + } else { + setRecentProjects([]); + setRecentProjectsDegraded(false); + setDegradedRefreshCount(0); + setLoading(true); + } + if (snapshot && !snapshot.isStale) { return; } void reload({ force: snapshot != null }); - }, [reload]); + }, [activeContextId, reload]); useEffect(() => { if (!recentProjectsDegraded) { @@ -225,22 +260,21 @@ export function useRecentProjectsSection( }); }, [aliveTeams, provisioningSnapshotByTeam, provisioningTeamNames, teams]); - const decoratedCards = useMemo( - () => - adaptRecentProjectsSection({ - projects: sortRecentProjectsByDisplayPriority(recentProjects), - taskCountsByProject, - activeTeamsByProject, - tasksLoading: globalTasksLoading, - }), - [ - activeTeamsByProject, - globalTasksLoading, - openHistoryVersion, - recentProjects, + const decoratedCards = useMemo(() => { + void openHistoryVersion; + return adaptRecentProjectsSection({ + projects: sortRecentProjectsByDisplayPriority(recentProjects), taskCountsByProject, - ] - ); + activeTeamsByProject, + tasksLoading: globalTasksLoading, + }); + }, [ + activeTeamsByProject, + globalTasksLoading, + openHistoryVersion, + recentProjects, + taskCountsByProject, + ]); const filteredCards = useMemo( () => decoratedCards.filter((card) => matchesSearch(card.project, searchQuery)), diff --git a/src/features/recent-projects/renderer/utils/recentProjectsClientCache.ts b/src/features/recent-projects/renderer/utils/recentProjectsClientCache.ts index e6c18df8..a01f40ae 100644 --- a/src/features/recent-projects/renderer/utils/recentProjectsClientCache.ts +++ b/src/features/recent-projects/renderer/utils/recentProjectsClientCache.ts @@ -9,8 +9,9 @@ const RECENT_PROJECTS_CLIENT_CACHE_TTL_MS = 15_000; const RECENT_PROJECTS_CLIENT_DEGRADED_CACHE_TTL_MS = 30_000; let cachedPayload: DashboardRecentProjectsPayloadLike = null; +let cachedKey: string | null = null; let cachedAt = 0; -let inFlightLoad: Promise | null = null; +let inFlightLoad: { key: string; promise: Promise } | null = null; export interface RecentProjectsClientSnapshot { payload: DashboardRecentProjectsPayload; @@ -18,7 +19,13 @@ export interface RecentProjectsClientSnapshot { isStale: boolean; } -export function getRecentProjectsClientSnapshot(): RecentProjectsClientSnapshot | null { +export function getRecentProjectsClientSnapshot( + cacheKey: string +): RecentProjectsClientSnapshot | null { + if (cachedKey !== cacheKey) { + return null; + } + const normalizedPayload = normalizeDashboardRecentProjectsPayload(cachedPayload); if (!normalizedPayload) { return null; @@ -40,39 +47,44 @@ export function getRecentProjectsClientSnapshot(): RecentProjectsClientSnapshot } export async function loadRecentProjectsWithClientCache( + cacheKey: string, loader: () => Promise, options?: { force?: boolean } ): Promise { const force = options?.force ?? false; - const snapshot = getRecentProjectsClientSnapshot(); + const snapshot = getRecentProjectsClientSnapshot(cacheKey); if (!force && snapshot && !snapshot.isStale) { return snapshot.payload; } - if (inFlightLoad) { - return inFlightLoad; + if (inFlightLoad?.key === cacheKey) { + return inFlightLoad.promise; } const request = loader() .then((payloadLike) => { const normalizedPayload = normalizeDashboardRecentProjectsPayload(payloadLike); - cachedPayload = normalizedPayload; - cachedAt = Date.now(); + if (inFlightLoad?.key === cacheKey && inFlightLoad.promise === request) { + cachedKey = normalizedPayload ? cacheKey : null; + cachedPayload = normalizedPayload; + cachedAt = Date.now(); + } return normalizedPayload ?? { projects: [], degraded: true }; }) .finally(() => { - if (inFlightLoad === request) { + if (inFlightLoad?.promise === request) { inFlightLoad = null; } }); - inFlightLoad = request; + inFlightLoad = { key: cacheKey, promise: request }; return request; } export function __resetRecentProjectsClientCacheForTests(): void { cachedPayload = null; + cachedKey = null; cachedAt = 0; inFlightLoad = null; } diff --git a/test/features/recent-projects/renderer/utils/recentProjectsClientCache.test.ts b/test/features/recent-projects/renderer/utils/recentProjectsClientCache.test.ts index bf14e3b1..c2ab0268 100644 --- a/test/features/recent-projects/renderer/utils/recentProjectsClientCache.test.ts +++ b/test/features/recent-projects/renderer/utils/recentProjectsClientCache.test.ts @@ -1,10 +1,9 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; - import { __resetRecentProjectsClientCacheForTests, getRecentProjectsClientSnapshot, loadRecentProjectsWithClientCache, } from '@features/recent-projects/renderer/utils/recentProjectsClientCache'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import type { DashboardRecentProject, @@ -33,6 +32,19 @@ const payload = ( degraded: false, ...overrides, }); +const LOCAL_CACHE_KEY = 'local'; +const SSH_CACHE_KEY = 'ssh-dev'; + +function deferred(): { + promise: Promise; + resolve: (value: T) => void; +} { + let resolve!: (value: T) => void; + const promise = new Promise((promiseResolve) => { + resolve = promiseResolve; + }); + return { promise, resolve }; +} describe('recentProjectsClientCache', () => { afterEach(() => { @@ -44,11 +56,15 @@ describe('recentProjectsClientCache', () => { it('returns cached projects while the client cache is fresh', async () => { const loader = vi.fn().mockResolvedValue(payload('alpha')); - await expect(loadRecentProjectsWithClientCache(loader)).resolves.toEqual(payload('alpha')); - await expect(loadRecentProjectsWithClientCache(loader)).resolves.toEqual(payload('alpha')); + await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual( + payload('alpha') + ); + await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual( + payload('alpha') + ); expect(loader).toHaveBeenCalledTimes(1); - expect(getRecentProjectsClientSnapshot()?.payload).toEqual(payload('alpha')); + expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)?.payload).toEqual(payload('alpha')); }); it('revalidates stale cache without dropping the previous snapshot', async () => { @@ -60,20 +76,20 @@ describe('recentProjectsClientCache', () => { .mockResolvedValueOnce(payload('alpha')) .mockResolvedValueOnce(payload('beta')); - await loadRecentProjectsWithClientCache(loader); + await loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader); vi.setSystemTime(new Date('2026-04-14T12:00:16.000Z')); - expect(getRecentProjectsClientSnapshot()).toMatchObject({ + expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({ payload: payload('alpha'), isStale: true, }); - await expect(loadRecentProjectsWithClientCache(loader, { force: true })).resolves.toEqual( - payload('beta') - ); + await expect( + loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader, { force: true }) + ).resolves.toEqual(payload('beta')); expect(loader).toHaveBeenCalledTimes(2); - expect(getRecentProjectsClientSnapshot()).toMatchObject({ + expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({ payload: payload('beta'), isStale: false, }); @@ -92,8 +108,8 @@ describe('recentProjectsClientCache', () => { }) ); - const first = loadRecentProjectsWithClientCache(loader, { force: true }); - const second = loadRecentProjectsWithClientCache(loader, { force: true }); + const first = loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader, { force: true }); + const second = loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader, { force: true }); expect(loader).toHaveBeenCalledTimes(1); @@ -111,24 +127,24 @@ describe('recentProjectsClientCache', () => { .fn<() => Promise>() .mockResolvedValueOnce(payload('alpha', { degraded: true })); - await expect(loadRecentProjectsWithClientCache(loader)).resolves.toEqual( + await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual( payload('alpha', { degraded: true }) ); vi.setSystemTime(new Date('2026-04-14T12:00:01.000Z')); - expect(getRecentProjectsClientSnapshot()).toMatchObject({ + expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({ payload: payload('alpha', { degraded: true }), isStale: false, }); vi.setSystemTime(new Date('2026-04-14T12:00:20.000Z')); - expect(getRecentProjectsClientSnapshot()).toMatchObject({ + expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({ payload: payload('alpha', { degraded: true }), isStale: false, }); vi.setSystemTime(new Date('2026-04-14T12:00:31.000Z')); - expect(getRecentProjectsClientSnapshot()).toMatchObject({ + expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toMatchObject({ payload: payload('alpha', { degraded: true }), isStale: true, }); @@ -139,7 +155,52 @@ describe('recentProjectsClientCache', () => { .fn<() => Promise>() .mockResolvedValue([project('alpha')]); - await expect(loadRecentProjectsWithClientCache(loader)).resolves.toEqual(payload('alpha')); - expect(getRecentProjectsClientSnapshot()?.payload).toEqual(payload('alpha')); + await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual( + payload('alpha') + ); + expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)?.payload).toEqual(payload('alpha')); + }); + + it('does not serve a cached payload across active context keys', async () => { + const loader = vi + .fn<() => Promise>() + .mockResolvedValueOnce(payload('local-alpha')) + .mockResolvedValueOnce(payload('ssh-beta')); + + await expect(loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader)).resolves.toEqual( + payload('local-alpha') + ); + + expect(getRecentProjectsClientSnapshot(SSH_CACHE_KEY)).toBeNull(); + await expect(loadRecentProjectsWithClientCache(SSH_CACHE_KEY, loader)).resolves.toEqual( + payload('ssh-beta') + ); + + expect(loader).toHaveBeenCalledTimes(2); + expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toBeNull(); + expect(getRecentProjectsClientSnapshot(SSH_CACHE_KEY)?.payload).toEqual(payload('ssh-beta')); + }); + + it('does not reuse or cache an in-flight payload for a different context key', async () => { + const localRequest = deferred(); + const sshRequest = deferred(); + const loader = vi + .fn<() => Promise>() + .mockReturnValueOnce(localRequest.promise) + .mockReturnValueOnce(sshRequest.promise); + + const localLoad = loadRecentProjectsWithClientCache(LOCAL_CACHE_KEY, loader, { force: true }); + const sshLoad = loadRecentProjectsWithClientCache(SSH_CACHE_KEY, loader, { force: true }); + + expect(loader).toHaveBeenCalledTimes(2); + + sshRequest.resolve(payload('ssh-beta')); + await expect(sshLoad).resolves.toEqual(payload('ssh-beta')); + expect(getRecentProjectsClientSnapshot(SSH_CACHE_KEY)?.payload).toEqual(payload('ssh-beta')); + + localRequest.resolve(payload('local-alpha')); + await expect(localLoad).resolves.toEqual(payload('local-alpha')); + expect(getRecentProjectsClientSnapshot(LOCAL_CACHE_KEY)).toBeNull(); + expect(getRecentProjectsClientSnapshot(SSH_CACHE_KEY)?.payload).toEqual(payload('ssh-beta')); }); }); From 96478a604f9033de4722f9d318d33c6756f9dbee Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 16:40:24 +0300 Subject: [PATCH 09/59] fix(recent-projects): close context switch response race --- .../renderer/hooks/useRecentProjectsSection.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts b/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts index 3eddb523..187851a1 100644 --- a/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts +++ b/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts @@ -110,6 +110,7 @@ export function useRecentProjectsSection( initialSnapshot?.payload.projects ?? [] ); const activeContextIdRef = useRef(activeContextId); + activeContextIdRef.current = activeContextId; const provisioningState = useMemo( () => ({ currentProvisioningRunIdByTeam, provisioningRuns }), [currentProvisioningRunIdByTeam, provisioningRuns] @@ -130,10 +131,6 @@ export function useRecentProjectsSection( recentProjectsRef.current = recentProjects; }, [recentProjects]); - useEffect(() => { - activeContextIdRef.current = activeContextId; - }, [activeContextId]); - const reload = useCallback( async (options?: { force?: boolean }): Promise => { const requestContextId = activeContextId; From e46868b6d7f340ee75f1d96afda9d69b4d083392 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 17:00:41 +0300 Subject: [PATCH 10/59] fix(context): reset team caches on context changes --- src/renderer/store/slices/connectionSlice.ts | 8 +- src/renderer/store/slices/contextSlice.ts | 12 +- src/renderer/store/slices/teamSlice.ts | 21 +- src/renderer/store/utils/stateResetHelpers.ts | 68 ++++ .../store/contextSliceTeamReset.test.ts | 304 ++++++++++++++++++ .../store/teamSliceContextRace.test.ts | 166 ++++++++++ 6 files changed, 576 insertions(+), 3 deletions(-) create mode 100644 test/renderer/store/contextSliceTeamReset.test.ts create mode 100644 test/renderer/store/teamSliceContextRace.test.ts diff --git a/src/renderer/store/slices/connectionSlice.ts b/src/renderer/store/slices/connectionSlice.ts index a16e3f6c..126852ec 100644 --- a/src/renderer/store/slices/connectionSlice.ts +++ b/src/renderer/store/slices/connectionSlice.ts @@ -7,7 +7,7 @@ import { api } from '@renderer/api'; -import { getFullResetState } from '../utils/stateResetHelpers'; +import { getContextScopedTeamResetState, getFullResetState } from '../utils/stateResetHelpers'; import type { AppState } from '../types'; import type { @@ -98,6 +98,7 @@ export const createConnectionSlice: StateCreator { return { ...getFullResetState(), + ...getContextScopedTeamResetState(), projects: [], projectsLoading: false, projectsInitialized: false, @@ -259,11 +260,17 @@ export const createContextSlice: StateCreator = // Fetch active context from main process const activeContextId = await api.context.getActive(); + const previousContextId = get().activeContextId; set({ + ...(activeContextId !== previousContextId ? getContextScopedTeamResetState() : {}), contextSnapshotsReady: true, activeContextId, }); + if (activeContextId !== previousContextId) { + void get().fetchTeams(); + void get().fetchAllTasks(); + } // Fetch available contexts await get().fetchAvailableContexts(); @@ -317,6 +324,7 @@ export const createContextSlice: StateCreator = // Step 2: Apply cached snapshot immediately for instant visual feedback if (targetSnapshot) { set({ + ...getContextScopedTeamResetState(), projects: targetSnapshot.projects, projectsLoading: false, projectsInitialized: true, @@ -402,6 +410,8 @@ export const createContextSlice: StateCreator = // Step 4: Fetch notifications in background void get().fetchNotifications(); + void get().fetchTeams(); + void get().fetchAllTasks(); } catch (error) { console.error('[contextSlice] Failed to switch context:', error); // Do NOT leave in broken state diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 9fc75c6a..04e15f42 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -236,6 +236,7 @@ const pendingFreshTeamMessagesHeadRefreshes = new Set(); const inFlightTeamMemberActivityMetaRequests = new Map>(); const pendingFreshTeamMemberActivityMetaRefreshes = new Set(); const pendingTeamPendingReplyRefreshTimers = new Map>(); +let latestTeamsFetchRequestId = 0; let inFlightGlobalTasksRefresh: Promise | null = null; let pendingFreshGlobalTasksRefresh = false; interface RefreshTeamDataOptions { @@ -286,6 +287,9 @@ export function __resetTeamSliceModuleStateForTests(): void { clearTimeout(timer); } pendingTeamPendingReplyRefreshTimers.clear(); + latestTeamsFetchRequestId = 0; + inFlightGlobalTasksRefresh = null; + pendingFreshGlobalTasksRefresh = false; clearAllPendingReplyRefreshWaits(); clearAllLastResolvedTeamDataRefreshes(); clearAllTeamLocalStateEpochs(); @@ -1401,6 +1405,8 @@ export const createTeamSlice: StateCreator = (set, // Only effective during initial load (when teamsLoading is set to true below). // Refreshes are already serialized by the throttle timer in onTeamChange. if (get().teamsLoading) return; + const requestContextId = get().activeContextId; + const requestId = ++latestTeamsFetchRequestId; // Only show loading spinner on initial load — avoids flickering when refreshing const isInitialLoad = get().teams.length === 0; if (isInitialLoad) { @@ -1412,6 +1418,9 @@ export const createTeamSlice: StateCreator = (set, TEAM_FETCH_TIMEOUT_MS, 'fetchTeams' ); + if (get().activeContextId !== requestContextId || latestTeamsFetchRequestId !== requestId) { + return; + } const teamByName: Record = {}; const teamBySessionId: Record = {}; for (const team of teams) { @@ -1444,6 +1453,9 @@ export const createTeamSlice: StateCreator = (set, }; }); } catch (error) { + if (get().activeContextId !== requestContextId || latestTeamsFetchRequestId !== requestId) { + return; + } // On refresh failure, keep existing teams visible set({ teamsLoading: false, @@ -1475,15 +1487,19 @@ export const createTeamSlice: StateCreator = (set, if (isInitialLoad) { set({ globalTasksLoading: true, globalTasksError: null }); } + const requestContextId = get().activeContextId; const oldTasks = get().globalTasks; - const wasFirst = consumeFirstGlobalTasksFetchFlag(); try { const tasks = await withTimeout( unwrapIpc('team:getAllTasks', () => api.teams.getAllTasks()), TEAM_FETCH_TIMEOUT_MS, 'fetchAllTasks' ); + if (get().activeContextId !== requestContextId) { + continue; + } const notificationState = get(); + const wasFirst = consumeFirstGlobalTasksFetchFlag(); processGlobalTaskNotifications({ oldTasks, newTasks: tasks, @@ -1499,6 +1515,9 @@ export const createTeamSlice: StateCreator = (set, globalTasksError: null, }); } catch (error) { + if (get().activeContextId !== requestContextId) { + continue; + } set({ globalTasksLoading: false, globalTasksInitialized: true, diff --git a/src/renderer/store/utils/stateResetHelpers.ts b/src/renderer/store/utils/stateResetHelpers.ts index 60ecab71..fd6991cb 100644 --- a/src/renderer/store/utils/stateResetHelpers.ts +++ b/src/renderer/store/utils/stateResetHelpers.ts @@ -53,6 +53,74 @@ export function getProjectSelectionResetState(): Partial { }; } +/** + * Reset team/task data that belongs to the active main-process context. + * These caches are populated through context-aware IPC calls and must not + * survive a local/SSH context switch. + */ +export function getContextScopedTeamResetState(): Partial { + return { + teams: [], + teamByName: {}, + teamBySessionId: {}, + branchByPath: {}, + teamsLoading: false, + teamsError: null, + globalTasks: [], + globalTasksLoading: false, + globalTasksInitialized: false, + globalTasksError: null, + globalTaskDetail: null, + pendingMemberProfile: null, + pendingTeamSectionFocus: null, + pendingReviewRequest: null, + selectedTeamName: null, + selectedTeamData: null, + teamDataCacheByName: {}, + selectedTeamLoading: false, + selectedTeamLoadNonce: 0, + selectedTeamError: null, + sendingMessage: false, + sendMessageError: null, + sendMessageWarning: null, + sendMessageDebugDetails: null, + lastSendMessageResult: null, + reviewActionError: null, + graphLayoutModeByTeam: {}, + gridOwnerOrderByTeam: {}, + slotAssignmentsByTeam: {}, + teamMessagesByName: {}, + memberActivityMetaByTeam: {}, + graphLayoutSessionByTeam: {}, + provisioningRuns: {}, + provisioningSnapshotByTeam: {}, + currentProvisioningRunIdByTeam: {}, + currentRuntimeRunIdByTeam: {}, + ignoredProvisioningRunIds: {}, + ignoredRuntimeRunIds: {}, + provisioningStartedAtFloorByTeam: {}, + leadActivityByTeam: {}, + leadContextByTeam: {}, + activeTaskLogActivityByTeam: {}, + activeToolsByTeam: {}, + finishedVisibleByTeam: {}, + toolHistoryByTeam: {}, + memberSpawnStatusesByTeam: {}, + memberSpawnSnapshotsByTeam: {}, + teamAgentRuntimeByTeam: {}, + provisioningErrorByTeam: {}, + crossTeamTargets: [], + crossTeamTargetsLoading: false, + kanbanFilterQuery: null, + addingComment: false, + addCommentError: null, + deletedTasks: [], + deletedTasksLoading: false, + pendingApprovals: [], + resolvedApprovals: new Map(), + }; +} + /** * Full state reset (session + project + repository + conversation). * Used when closing all tabs or resetting to initial state. diff --git a/test/renderer/store/contextSliceTeamReset.test.ts b/test/renderer/store/contextSliceTeamReset.test.ts new file mode 100644 index 00000000..5da55851 --- /dev/null +++ b/test/renderer/store/contextSliceTeamReset.test.ts @@ -0,0 +1,304 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { createTestStore } from './storeTestUtils'; + +const apiMock = vi.hoisted(() => ({ + context: { + switch: vi.fn(async () => undefined), + list: vi.fn(async () => [{ id: 'local', type: 'local' }]), + getActive: vi.fn(async () => 'local'), + onChanged: vi.fn(() => () => undefined), + }, + getProjects: vi.fn(async (): Promise => []), + getRepositoryGroups: vi.fn(async (): Promise => []), + notifications: { + get: vi.fn(async () => ({ + notifications: [], + total: 0, + totalCount: 0, + unreadCount: 0, + hasMore: false, + })), + }, + teams: { + list: vi.fn(async () => []), + getAllTasks: vi.fn(async () => []), + showMessageNotification: vi.fn(async () => undefined), + }, + ssh: { + connect: vi.fn(async () => ({ state: 'connected', host: 'dev', error: null })), + disconnect: vi.fn(async () => ({ state: 'disconnected', host: null, error: null })), + saveLastConnection: vi.fn(async () => undefined), + }, +})); + +const contextStorageMock = vi.hoisted(() => ({ + saveSnapshot: vi.fn(async () => undefined), + loadSnapshot: vi.fn(), + cleanupExpired: vi.fn(async () => undefined), + isAvailable: vi.fn(async () => true), +})); + +const draftStorageMock = vi.hoisted(() => ({ + cleanupExpired: vi.fn(async () => undefined), +})); + +vi.mock('@renderer/api', () => ({ + api: apiMock, +})); + +vi.mock('@renderer/services/contextStorage', () => ({ + contextStorage: contextStorageMock, +})); + +vi.mock('@renderer/services/draftStorage', () => ({ + draftStorage: draftStorageMock, +})); + +function targetSnapshot() { + return { + projects: [ + { + id: 'ssh-project', + name: 'SSH Project', + path: '/ssh/project', + sessions: [], + createdAt: new Date(0).toISOString(), + updatedAt: new Date(0).toISOString(), + }, + ], + selectedProjectId: null, + repositoryGroups: [], + selectedRepositoryId: null, + selectedWorktreeId: null, + viewMode: 'flat' as const, + sessions: [], + selectedSessionId: null, + sessionsCursor: null, + sessionsHasMore: false, + sessionsTotalCount: 0, + pinnedSessionIds: [], + notifications: [], + unreadCount: 0, + openTabs: [], + activeTabId: null, + selectedTabIds: [], + activeProjectId: null, + paneLayout: { + panes: [ + { + id: 'pane-default', + tabs: [], + activeTabId: null, + selectedTabIds: [], + widthFraction: 1, + }, + ], + focusedPaneId: 'pane-default', + }, + sidebarCollapsed: false, + _metadata: { + contextId: 'ssh-dev', + capturedAt: Date.now(), + version: 1, + }, + }; +} + +describe('context slice team/task reset', () => { + beforeEach(() => { + vi.clearAllMocks(); + contextStorageMock.loadSnapshot.mockResolvedValue(targetSnapshot()); + apiMock.context.getActive.mockResolvedValue('local'); + apiMock.getProjects.mockResolvedValue(targetSnapshot().projects); + apiMock.getRepositoryGroups.mockResolvedValue([]); + apiMock.teams.list.mockResolvedValue([]); + apiMock.teams.getAllTasks.mockResolvedValue([]); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('drops previous-context team and task caches before refreshing the target context', async () => { + const store = createTestStore(); + store.setState({ + activeContextId: 'local', + teams: [ + { + teamName: 'local-team', + displayName: 'Local Team', + projectPath: '/local/project', + }, + ], + teamByName: { + 'local-team': { + teamName: 'local-team', + displayName: 'Local Team', + projectPath: '/local/project', + }, + }, + teamBySessionId: {}, + globalTasks: [ + { + id: 'local-task', + subject: 'Local task', + status: 'todo', + teamName: 'local-team', + teamDisplayName: 'Local Team', + projectPath: '/local/project', + comments: [], + }, + ], + globalTasksInitialized: true, + selectedTeamName: 'local-team', + selectedTeamData: { teamName: 'local-team' }, + teamDataCacheByName: { 'local-team': { teamName: 'local-team' } }, + } as never); + + await store.getState().switchContext('ssh-dev'); + + expect(store.getState().activeContextId).toBe('ssh-dev'); + expect(store.getState().teams).toEqual([]); + expect(store.getState().teamByName).toEqual({}); + expect(store.getState().globalTasks).toEqual([]); + expect(store.getState().selectedTeamName).toBeNull(); + expect(store.getState().selectedTeamData).toBeNull(); + expect(store.getState().teamDataCacheByName).toEqual({}); + expect(apiMock.teams.list).toHaveBeenCalledTimes(1); + expect(apiMock.teams.getAllTasks).toHaveBeenCalledTimes(1); + }); + + it('drops previous-context team and task caches when lazy context initialization changes context', async () => { + apiMock.context.getActive.mockResolvedValue('ssh-dev'); + const store = createTestStore(); + store.setState({ + activeContextId: 'local', + teams: [ + { + teamName: 'local-team', + displayName: 'Local Team', + projectPath: '/local/project', + }, + ], + teamByName: { + 'local-team': { + teamName: 'local-team', + displayName: 'Local Team', + projectPath: '/local/project', + }, + }, + globalTasks: [ + { + id: 'local-task', + subject: 'Local task', + status: 'todo', + teamName: 'local-team', + teamDisplayName: 'Local Team', + projectPath: '/local/project', + comments: [], + }, + ], + globalTasksInitialized: true, + } as never); + + await store.getState().initializeContextSystem(); + + expect(store.getState().activeContextId).toBe('ssh-dev'); + expect(store.getState().teams).toEqual([]); + expect(store.getState().teamByName).toEqual({}); + expect(store.getState().globalTasks).toEqual([]); + expect(apiMock.teams.list).toHaveBeenCalledTimes(1); + expect(apiMock.teams.getAllTasks).toHaveBeenCalledTimes(1); + }); + + it('drops previous-context team and task caches on direct SSH connect', async () => { + const store = createTestStore(); + store.setState({ + activeContextId: 'local', + teams: [ + { + teamName: 'local-team', + displayName: 'Local Team', + projectPath: '/local/project', + }, + ], + teamByName: { + 'local-team': { + teamName: 'local-team', + displayName: 'Local Team', + projectPath: '/local/project', + }, + }, + globalTasks: [ + { + id: 'local-task', + subject: 'Local task', + status: 'todo', + teamName: 'local-team', + teamDisplayName: 'Local Team', + projectPath: '/local/project', + comments: [], + }, + ], + globalTasksInitialized: true, + } as never); + + await store.getState().connectSsh({ + host: 'dev', + port: 22, + username: 'me', + authMethod: 'privateKey', + privateKeyPath: '/tmp/key', + }); + + expect(store.getState().activeContextId).toBe('ssh-dev'); + expect(store.getState().teams).toEqual([]); + expect(store.getState().teamByName).toEqual({}); + expect(store.getState().globalTasks).toEqual([]); + expect(apiMock.teams.list).toHaveBeenCalledTimes(1); + expect(apiMock.teams.getAllTasks).toHaveBeenCalledTimes(1); + }); + + it('drops previous-context team and task caches on direct SSH disconnect', async () => { + const store = createTestStore(); + store.setState({ + activeContextId: 'ssh-dev', + teams: [ + { + teamName: 'ssh-team', + displayName: 'SSH Team', + projectPath: '/ssh/project', + }, + ], + teamByName: { + 'ssh-team': { + teamName: 'ssh-team', + displayName: 'SSH Team', + projectPath: '/ssh/project', + }, + }, + globalTasks: [ + { + id: 'ssh-task', + subject: 'SSH task', + status: 'todo', + teamName: 'ssh-team', + teamDisplayName: 'SSH Team', + projectPath: '/ssh/project', + comments: [], + }, + ], + globalTasksInitialized: true, + } as never); + + await store.getState().disconnectSsh(); + + expect(store.getState().activeContextId).toBe('local'); + expect(store.getState().teams).toEqual([]); + expect(store.getState().teamByName).toEqual({}); + expect(store.getState().globalTasks).toEqual([]); + expect(apiMock.teams.list).toHaveBeenCalledTimes(1); + expect(apiMock.teams.getAllTasks).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/renderer/store/teamSliceContextRace.test.ts b/test/renderer/store/teamSliceContextRace.test.ts new file mode 100644 index 00000000..a9374ea5 --- /dev/null +++ b/test/renderer/store/teamSliceContextRace.test.ts @@ -0,0 +1,166 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { create } from 'zustand'; + +import { + __resetTeamSliceModuleStateForTests, + createTeamSlice, +} from '../../../src/renderer/store/slices/teamSlice'; + +import type { AppState } from '../../../src/renderer/store/types'; + +const apiMock = vi.hoisted(() => ({ + teams: { + list: vi.fn(), + getAllTasks: vi.fn(), + showMessageNotification: vi.fn(async () => undefined), + }, +})); + +interface TeamSummaryLike { + teamName: string; + displayName: string; + projectPath: string; +} + +interface GlobalTaskLike { + id: string; + subject: string; + status: string; + teamName: string; + teamDisplayName: string; + projectPath: string; + comments: []; +} + +vi.mock('@renderer/api', () => ({ + api: apiMock, +})); + +function deferred(): { + promise: Promise; + resolve: (value: T) => void; +} { + let resolve!: (value: T) => void; + const promise = new Promise((innerResolve) => { + resolve = innerResolve; + }); + return { promise, resolve }; +} + +function createSliceStore() { + return create()((set, get, store) => + ({ + ...createTeamSlice(set as never, get as never, store as never), + activeContextId: 'local', + appConfig: null, + paneLayout: { + focusedPaneId: 'pane-default', + panes: [ + { + id: 'pane-default', + widthFraction: 1, + tabs: [], + activeTabId: null, + }, + ], + }, + openTab: vi.fn(), + setActiveTab: vi.fn(), + updateTabLabel: vi.fn(), + getAllPaneTabs: vi.fn(() => []), + warmTaskChangeSummaries: vi.fn(async () => undefined), + invalidateTaskChangePresence: vi.fn(), + }) as unknown as AppState + ); +} + +describe('team slice context races', () => { + beforeEach(() => { + __resetTeamSliceModuleStateForTests(); + apiMock.teams.list.mockReset(); + apiMock.teams.getAllTasks.mockReset(); + apiMock.teams.showMessageNotification.mockClear(); + }); + + afterEach(() => { + __resetTeamSliceModuleStateForTests(); + vi.restoreAllMocks(); + }); + + it('ignores a team list response loaded for a previous context', async () => { + const store = createSliceStore(); + const localList = deferred(); + apiMock.teams.list.mockReturnValueOnce(localList.promise); + + const fetchPromise = store.getState().fetchTeams(); + expect(store.getState().teamsLoading).toBe(true); + + store.setState({ + activeContextId: 'ssh-dev', + teams: [], + teamByName: {}, + teamBySessionId: {}, + teamsLoading: false, + }); + localList.resolve([ + { + teamName: 'local-team', + displayName: 'Local Team', + projectPath: '/local/project', + }, + ]); + await fetchPromise; + + expect(store.getState().teams).toEqual([]); + expect(store.getState().teamByName).toEqual({}); + expect(store.getState().teamsLoading).toBe(false); + }); + + it('reruns a pending global task refresh for the current context instead of applying stale tasks', async () => { + const store = createSliceStore(); + const localTasks = deferred(); + apiMock.teams.getAllTasks.mockReturnValueOnce(localTasks.promise).mockResolvedValueOnce([ + { + id: 'ssh-task', + subject: 'SSH task', + status: 'todo', + teamName: 'ssh-team', + teamDisplayName: 'SSH Team', + projectPath: '/ssh/project', + comments: [], + }, + ]); + + const firstFetch = store.getState().fetchAllTasks(); + expect(store.getState().globalTasksLoading).toBe(true); + + store.setState({ + activeContextId: 'ssh-dev', + globalTasks: [], + globalTasksLoading: false, + globalTasksInitialized: false, + }); + const secondFetch = store.getState().fetchAllTasks(); + + localTasks.resolve([ + { + id: 'local-task', + subject: 'Local task', + status: 'todo', + teamName: 'local-team', + teamDisplayName: 'Local Team', + projectPath: '/local/project', + comments: [], + }, + ]); + + await Promise.all([firstFetch, secondFetch]); + + expect(apiMock.teams.getAllTasks).toHaveBeenCalledTimes(2); + expect(store.getState().globalTasks).toEqual([ + expect.objectContaining({ id: 'ssh-task', teamName: 'ssh-team' }), + ]); + expect(store.getState().globalTasksInitialized).toBe(true); + expect(store.getState().globalTasksLoading).toBe(false); + }); +}); From 255fa5aa476c5696f49659901384ce0cb4a44553 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 17:04:51 +0300 Subject: [PATCH 11/59] fix(context): align first-visit switch state --- src/renderer/store/slices/contextSlice.ts | 7 +++ .../store/contextSliceTeamReset.test.ts | 56 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/renderer/store/slices/contextSlice.ts b/src/renderer/store/slices/contextSlice.ts index e32560c4..fb7850e2 100644 --- a/src/renderer/store/slices/contextSlice.ts +++ b/src/renderer/store/slices/contextSlice.ts @@ -356,6 +356,13 @@ export const createContextSlice: StateCreator = isContextSwitching: false, targetContextId: null, }); + } else { + set({ + ...getEmptyContextState(), + activeContextId: targetContextId, + isContextSwitching: true, + targetContextId, + }); } // Step 3: Fetch fresh data in background (slow over SSH) diff --git a/test/renderer/store/contextSliceTeamReset.test.ts b/test/renderer/store/contextSliceTeamReset.test.ts index 5da55851..6497d6a4 100644 --- a/test/renderer/store/contextSliceTeamReset.test.ts +++ b/test/renderer/store/contextSliceTeamReset.test.ts @@ -105,6 +105,17 @@ function targetSnapshot() { }; } +function deferred(): { + promise: Promise; + resolve: (value: T) => void; +} { + let resolve!: (value: T) => void; + const promise = new Promise((innerResolve) => { + resolve = innerResolve; + }); + return { promise, resolve }; +} + describe('context slice team/task reset', () => { beforeEach(() => { vi.clearAllMocks(); @@ -169,6 +180,51 @@ describe('context slice team/task reset', () => { expect(apiMock.teams.getAllTasks).toHaveBeenCalledTimes(1); }); + it('updates the active context before slow first-visit project scans can trigger team refreshes', async () => { + contextStorageMock.loadSnapshot.mockResolvedValue(null); + const projectScan = deferred(); + apiMock.getProjects.mockReturnValue(projectScan.promise); + apiMock.getRepositoryGroups.mockResolvedValue([]); + const store = createTestStore(); + store.setState({ + activeContextId: 'local', + teams: [ + { + teamName: 'local-team', + displayName: 'Local Team', + projectPath: '/local/project', + }, + ], + globalTasks: [ + { + id: 'local-task', + subject: 'Local task', + status: 'todo', + teamName: 'local-team', + teamDisplayName: 'Local Team', + projectPath: '/local/project', + comments: [], + }, + ], + globalTasksInitialized: true, + } as never); + + const switchPromise = store.getState().switchContext('ssh-dev'); + await Promise.resolve(); + await Promise.resolve(); + + expect(store.getState().activeContextId).toBe('ssh-dev'); + expect(store.getState().isContextSwitching).toBe(true); + expect(store.getState().teams).toEqual([]); + expect(store.getState().globalTasks).toEqual([]); + + projectScan.resolve(targetSnapshot().projects); + await switchPromise; + + expect(store.getState().activeContextId).toBe('ssh-dev'); + expect(store.getState().isContextSwitching).toBe(false); + }); + it('drops previous-context team and task caches when lazy context initialization changes context', async () => { apiMock.context.getActive.mockResolvedValue('ssh-dev'); const store = createTestStore(); From c04a259cea7cf63933de9d380a0d6104392a613a Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 17:35:17 +0300 Subject: [PATCH 12/59] fix(context): ignore stale team request scopes --- src/renderer/store/slices/connectionSlice.ts | 5 + src/renderer/store/slices/contextSlice.ts | 34 +- src/renderer/store/slices/teamSlice.ts | 156 +++++-- .../store/utils/contextScopedRequestEpoch.ts | 17 + .../store/contextSliceTeamReset.test.ts | 54 +++ .../store/teamSliceContextRace.test.ts | 420 +++++++++++++++++- 6 files changed, 643 insertions(+), 43 deletions(-) create mode 100644 src/renderer/store/utils/contextScopedRequestEpoch.ts diff --git a/src/renderer/store/slices/connectionSlice.ts b/src/renderer/store/slices/connectionSlice.ts index 126852ec..a59b23e7 100644 --- a/src/renderer/store/slices/connectionSlice.ts +++ b/src/renderer/store/slices/connectionSlice.ts @@ -7,6 +7,7 @@ import { api } from '@renderer/api'; +import { invalidateContextScopedRequestEpoch } from '../utils/contextScopedRequestEpoch'; import { getContextScopedTeamResetState, getFullResetState } from '../utils/stateResetHelpers'; import type { AppState } from '../types'; @@ -71,6 +72,9 @@ export const createConnectionSlice: StateCreator => { try { const status = await api.ssh.disconnect(); + invalidateContextScopedRequestEpoch(); set({ connectionMode: 'local', connectionState: status.state, diff --git a/src/renderer/store/slices/contextSlice.ts b/src/renderer/store/slices/contextSlice.ts index fb7850e2..4b016d27 100644 --- a/src/renderer/store/slices/contextSlice.ts +++ b/src/renderer/store/slices/contextSlice.ts @@ -9,6 +9,11 @@ import { api } from '@renderer/api'; import { contextStorage } from '@renderer/services/contextStorage'; import { draftStorage } from '@renderer/services/draftStorage'; +import { + captureContextScopedRequestEpoch, + invalidateContextScopedRequestEpoch, + isContextScopedRequestEpochCurrent, +} from '../utils/contextScopedRequestEpoch'; import { getContextScopedTeamResetState, getFullResetState } from '../utils/stateResetHelpers'; import type { AppState } from '../types'; @@ -78,6 +83,16 @@ function getEmptyContextState(): Partial { }; } +function isContextSwitchRequestCurrent( + get: () => AppState, + targetContextId: string, + requestEpoch: number +): boolean { + return ( + get().activeContextId === targetContextId && isContextScopedRequestEpochCurrent(requestEpoch) + ); +} + /** * Validate snapshot against fresh data from target context. * Filters invalid tabs, selections, and ensures at-least-one-pane invariant. @@ -261,13 +276,17 @@ export const createContextSlice: StateCreator = // Fetch active context from main process const activeContextId = await api.context.getActive(); const previousContextId = get().activeContextId; + const contextChanged = activeContextId !== previousContextId; + if (contextChanged) { + invalidateContextScopedRequestEpoch(); + } set({ - ...(activeContextId !== previousContextId ? getContextScopedTeamResetState() : {}), + ...(contextChanged ? getContextScopedTeamResetState() : {}), contextSnapshotsReady: true, activeContextId, }); - if (activeContextId !== previousContextId) { + if (contextChanged) { void get().fetchTeams(); void get().fetchAllTasks(); } @@ -320,6 +339,7 @@ export const createContextSlice: StateCreator = contextStorage.loadSnapshot(targetContextId), api.context.switch(targetContextId), ]); + invalidateContextScopedRequestEpoch(); // Step 2: Apply cached snapshot immediately for instant visual feedback if (targetSnapshot) { @@ -364,6 +384,7 @@ export const createContextSlice: StateCreator = targetContextId, }); } + const switchRequestEpoch = captureContextScopedRequestEpoch(); // Step 3: Fetch fresh data in background (slow over SSH) // Wrapped in try/catch so fetch failures don't wipe valid snapshot data. @@ -373,6 +394,9 @@ export const createContextSlice: StateCreator = api.getProjects(), api.getRepositoryGroups(), ]); + if (!isContextSwitchRequestCurrent(get, targetContextId, switchRequestEpoch)) { + return; + } if (targetSnapshot) { // Guard: don't overwrite snapshot data if fetch returned empty @@ -403,6 +427,9 @@ export const createContextSlice: StateCreator = } } catch (fetchError) { console.error('[contextSlice] Background data refresh failed:', fetchError); + if (!isContextSwitchRequestCurrent(get, targetContextId, switchRequestEpoch)) { + return; + } // Keep snapshot data as fallback — don't wipe user's view if (!targetSnapshot) { // No snapshot and fetch failed — finalize switch with empty state @@ -416,6 +443,9 @@ export const createContextSlice: StateCreator = } // Step 4: Fetch notifications in background + if (!isContextSwitchRequestCurrent(get, targetContextId, switchRequestEpoch)) { + return; + } void get().fetchNotifications(); void get().fetchTeams(); void get().fetchAllTasks(); diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 04e15f42..423665fc 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -131,6 +131,11 @@ import { import { structurallyShareTeamSnapshot } from '../team/teamSnapshotStructuralSharing'; import { parseToolApprovalSettings } from '../team/teamToolApprovalSettings'; import { noteTeamRefreshFanout } from '../teamRefreshFanoutDiagnostics'; +import { + captureContextScopedRequestEpoch, + isContextScopedRequestEpochCurrent, + resetContextScopedRequestEpochForTests, +} from '../utils/contextScopedRequestEpoch'; import { getWorktreeNavigationState } from '../utils/stateResetHelpers'; import type { @@ -293,6 +298,7 @@ export function __resetTeamSliceModuleStateForTests(): void { clearAllPendingReplyRefreshWaits(); clearAllLastResolvedTeamDataRefreshes(); clearAllTeamLocalStateEpochs(); + resetContextScopedRequestEpochForTests(); clearAllMemberSpawnStatusesIpcBackoffs(); clearAllTeamRefreshBurstDiagnostics(); clearAllMemberSpawnUiEqualLastWarns(); @@ -427,15 +433,56 @@ function drainQueuedFullRefreshAfterThinSettles(teamName: string, get: () => Tea void get().refreshTeamData(teamName, { withDedup: true }); } +interface ContextRequestScope { + contextId: string; + contextEpoch: number; +} + +interface TeamRequestScope extends ContextRequestScope { + teamStateEpoch: number; +} + +function captureContextRequestScope(get: () => AppState): ContextRequestScope { + return { + contextId: get().activeContextId, + contextEpoch: captureContextScopedRequestEpoch(), + }; +} + +function isContextRequestScopeCurrent(get: () => AppState, scope: ContextRequestScope): boolean { + return ( + get().activeContextId === scope.contextId && + isContextScopedRequestEpochCurrent(scope.contextEpoch) + ); +} + +function captureTeamRequestScope(get: () => AppState, teamName: string): TeamRequestScope { + return { + ...captureContextRequestScope(get), + teamStateEpoch: captureTeamLocalStateEpoch(teamName), + }; +} + +function isTeamRequestScopeCurrent( + get: () => AppState, + teamName: string, + scope: TeamRequestScope +): boolean { + return ( + isContextRequestScopeCurrent(get, scope) && + isTeamLocalStateEpochCurrent(teamName, scope.teamStateEpoch) + ); +} + function isSelectedTeamLoadStillCurrent( - get: () => TeamSlice, + get: () => AppState, teamName: string, requestNonce: number, - teamStateEpoch: number + requestScope: TeamRequestScope ): boolean { const state = get(); return ( - isTeamLocalStateEpochCurrent(teamName, teamStateEpoch) && + isTeamRequestScopeCurrent(get, teamName, requestScope) && state.selectedTeamName === teamName && state.selectedTeamLoadNonce === requestNonce && state.selectedTeamData?.teamName === teamName @@ -445,10 +492,10 @@ function isSelectedTeamLoadStillCurrent( function schedulePostPaintTeamEnrichments(params: { teamName: string; requestNonce: number; - teamStateEpoch: number; - get: () => TeamSlice; + requestScope: TeamRequestScope; + get: () => AppState; }): void { - const { teamName, requestNonce, teamStateEpoch, get } = params; + const { teamName, requestNonce, requestScope, get } = params; cancelPostPaintTeamEnrichments(teamName); @@ -459,7 +506,7 @@ function schedulePostPaintTeamEnrichments(params: { postPaintTeamEnrichmentTimers.delete(teamName); void (async () => { - if (!isTeamLocalStateEpochCurrent(teamName, teamStateEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { queuedFullTeamDataRefreshesAfterThin.delete(teamName); return; } @@ -485,7 +532,7 @@ function schedulePostPaintTeamEnrichments(params: { try { const headResult = await get().refreshTeamMessagesHead(teamName); - if (!isSelectedTeamLoadStillCurrent(get, teamName, requestNonce, teamStateEpoch)) { + if (!isSelectedTeamLoadStillCurrent(get, teamName, requestNonce, requestScope)) { return; } if (headResult.feedChanged || isMemberActivityMetaStale(get(), teamName)) { @@ -1228,8 +1275,12 @@ export const createTeamSlice: StateCreator = (set, if (isMemberSpawnStatusesIpcBackoffActive(teamName)) { return; } + const requestScope = captureTeamRequestScope(get, teamName); try { const snapshot = await api.teams.getMemberSpawnStatuses(teamName); + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { + return; + } clearMemberSpawnStatusesIpcBackoff(teamName); set((prev) => { if (snapshot.runId != null && prev.ignoredRuntimeRunIds[snapshot.runId] === teamName) { @@ -1292,6 +1343,9 @@ export const createTeamSlice: StateCreator = (set, }; }); } catch (error) { + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { + return; + } const message = error instanceof Error ? error.message : String(error); if (message.includes("No handler registered for 'team:memberSpawnStatuses'")) { recordMemberSpawnStatusesIpcRetryBackoff( @@ -1304,8 +1358,12 @@ export const createTeamSlice: StateCreator = (set, }, fetchTeamAgentRuntime: async (teamName: string) => { if (!api.teams?.getTeamAgentRuntime) return; + const requestScope = captureTeamRequestScope(get, teamName); try { const snapshot = await api.teams.getTeamAgentRuntime(teamName); + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { + return; + } set((prev) => { if (snapshot.runId != null && prev.ignoredRuntimeRunIds[snapshot.runId] === teamName) { return {}; @@ -1405,7 +1463,7 @@ export const createTeamSlice: StateCreator = (set, // Only effective during initial load (when teamsLoading is set to true below). // Refreshes are already serialized by the throttle timer in onTeamChange. if (get().teamsLoading) return; - const requestContextId = get().activeContextId; + const requestScope = captureContextRequestScope(get); const requestId = ++latestTeamsFetchRequestId; // Only show loading spinner on initial load — avoids flickering when refreshing const isInitialLoad = get().teams.length === 0; @@ -1418,7 +1476,10 @@ export const createTeamSlice: StateCreator = (set, TEAM_FETCH_TIMEOUT_MS, 'fetchTeams' ); - if (get().activeContextId !== requestContextId || latestTeamsFetchRequestId !== requestId) { + if ( + !isContextRequestScopeCurrent(get, requestScope) || + latestTeamsFetchRequestId !== requestId + ) { return; } const teamByName: Record = {}; @@ -1453,7 +1514,10 @@ export const createTeamSlice: StateCreator = (set, }; }); } catch (error) { - if (get().activeContextId !== requestContextId || latestTeamsFetchRequestId !== requestId) { + if ( + !isContextRequestScopeCurrent(get, requestScope) || + latestTeamsFetchRequestId !== requestId + ) { return; } // On refresh failure, keep existing teams visible @@ -1487,7 +1551,7 @@ export const createTeamSlice: StateCreator = (set, if (isInitialLoad) { set({ globalTasksLoading: true, globalTasksError: null }); } - const requestContextId = get().activeContextId; + const requestScope = captureContextRequestScope(get); const oldTasks = get().globalTasks; try { const tasks = await withTimeout( @@ -1495,7 +1559,7 @@ export const createTeamSlice: StateCreator = (set, TEAM_FETCH_TIMEOUT_MS, 'fetchAllTasks' ); - if (get().activeContextId !== requestContextId) { + if (!isContextRequestScopeCurrent(get, requestScope)) { continue; } const notificationState = get(); @@ -1515,7 +1579,7 @@ export const createTeamSlice: StateCreator = (set, globalTasksError: null, }); } catch (error) { - if (get().activeContextId !== requestContextId) { + if (!isContextRequestScopeCurrent(get, requestScope)) { continue; } set({ @@ -2043,6 +2107,7 @@ export const createTeamSlice: StateCreator = (set, }, refreshTeamChangePresence: async (teamName: string) => { + const requestScope = captureTeamRequestScope(get, teamName); const currentTeamData = selectTeamDataForName(get(), teamName); if (!currentTeamData) { return; @@ -2052,6 +2117,9 @@ export const createTeamSlice: StateCreator = (set, const presenceByTaskId = await unwrapIpc('team:getTaskChangePresence', () => api.teams.getTaskChangePresence(teamName) ); + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { + return; + } set((state) => { const teamData = selectTeamDataForName(state, teamName); @@ -2099,7 +2167,7 @@ export const createTeamSlice: StateCreator = (set, }, selectTeam: async (teamName: string, opts) => { - const teamStateEpoch = captureTeamLocalStateEpoch(teamName); + const requestScope = captureTeamRequestScope(get, teamName); const allowReloadWhileProvisioning = opts?.allowReloadWhileProvisioning === true; // Guard: prevent duplicate in-flight fetches for the same team. // GlobalTaskDetailDialog + tab navigation can call selectTeam() in quick succession. @@ -2132,7 +2200,7 @@ export const createTeamSlice: StateCreator = (set, const data = await fetchTeamDataDeduped(teamName, { includeMemberBranches: false, }); - if (!isTeamLocalStateEpochCurrent(teamName, teamStateEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { queuedFullTeamDataRefreshesAfterThin.delete(teamName); return; } @@ -2266,7 +2334,7 @@ export const createTeamSlice: StateCreator = (set, if ( !opts?.skipProjectAutoSelect && projectPath && - isSelectedTeamLoadStillCurrent(get, teamName, requestNonce, teamStateEpoch) + isSelectedTeamLoadStillCurrent(get, teamName, requestNonce, requestScope) ) { const state = get(); const normalizedTeamPath = normalizePath(projectPath); @@ -2305,7 +2373,7 @@ export const createTeamSlice: StateCreator = (set, schedulePostPaintTeamEnrichments({ teamName, requestNonce, - teamStateEpoch, + requestScope, get, }); } catch (error) { @@ -2316,7 +2384,7 @@ export const createTeamSlice: StateCreator = (set, ); } } catch (error) { - if (!isTeamLocalStateEpochCurrent(teamName, teamStateEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { queuedFullTeamDataRefreshesAfterThin.delete(teamName); return; } @@ -2398,7 +2466,7 @@ export const createTeamSlice: StateCreator = (set, return; } - const teamStateEpoch = captureTeamLocalStateEpoch(teamName); + const requestScope = captureTeamRequestScope(get, teamName); const refreshToken = beginInFlightTeamDataRefresh(teamName); // Silent refresh — update data without showing loading skeleton. // Only selectTeam() sets loading: true (for initial load). @@ -2411,7 +2479,7 @@ export const createTeamSlice: StateCreator = (set, const data = opts?.withDedup ? await fetchTeamDataDeduped(teamName) : await fetchTeamDataFresh(teamName); - if (!isTeamLocalStateEpochCurrent(teamName, teamStateEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { return; } const projectedTeamData = previousData @@ -2462,7 +2530,7 @@ export const createTeamSlice: StateCreator = (set, await api.review.invalidateTaskChangeSummaries(teamName, invalidationState.taskIds); } } catch (error) { - if (!isTeamLocalStateEpochCurrent(teamName, teamStateEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { return; } const msg = @@ -2524,7 +2592,11 @@ export const createTeamSlice: StateCreator = (set, set({ selectedTeamError: msg }); } finally { endInFlightTeamDataRefresh(teamName, refreshToken); - if (reusedInFlightRequest && pendingFreshTeamDataRefreshes.delete(teamName)) { + if ( + reusedInFlightRequest && + pendingFreshTeamDataRefreshes.delete(teamName) && + isTeamRequestScopeCurrent(get, teamName, requestScope) + ) { void get().refreshTeamData(teamName); } } @@ -2543,10 +2615,10 @@ export const createTeamSlice: StateCreator = (set, const existingOlderRequest = inFlightTeamMessagesOlderRequests.get(teamName); if (existingOlderRequest) { - const queuedEpoch = captureTeamLocalStateEpoch(teamName); + const queuedScope = captureTeamRequestScope(get, teamName); const queuedRequest: Promise = existingOlderRequest .then(() => { - if (!isTeamLocalStateEpochCurrent(teamName, queuedEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, queuedScope)) { return { feedChanged: false, headChanged: false, @@ -2577,7 +2649,7 @@ export const createTeamSlice: StateCreator = (set, current: null, }; requestRef.current = (async (): Promise => { - const teamStateEpoch = captureTeamLocalStateEpoch(teamName); + const requestScope = captureTeamRequestScope(get, teamName); set((state) => ({ teamMessagesByName: { ...state.teamMessagesByName, @@ -2592,7 +2664,7 @@ export const createTeamSlice: StateCreator = (set, const page = await unwrapIpc('team:getMessagesPage', () => api.teams.getMessagesPage(teamName, { limit: 50 }) ); - if (!isTeamLocalStateEpochCurrent(teamName, teamStateEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { return { feedChanged: false, headChanged: false, @@ -2648,7 +2720,7 @@ export const createTeamSlice: StateCreator = (set, feedRevision: page.feedRevision, }; } catch (error) { - if (!isTeamLocalStateEpochCurrent(teamName, teamStateEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { return { feedChanged: false, headChanged: false, @@ -2668,7 +2740,10 @@ export const createTeamSlice: StateCreator = (set, } finally { if (inFlightTeamMessagesHeadRequests.get(teamName) === requestRef.current) { inFlightTeamMessagesHeadRequests.delete(teamName); - if (pendingFreshTeamMessagesHeadRefreshes.delete(teamName)) { + if ( + pendingFreshTeamMessagesHeadRefreshes.delete(teamName) && + isTeamRequestScopeCurrent(get, teamName, requestScope) + ) { void get().refreshTeamMessagesHead(teamName); } } @@ -2681,7 +2756,7 @@ export const createTeamSlice: StateCreator = (set, }, loadOlderTeamMessages: async (teamName: string) => { - const requestedEpoch = captureTeamLocalStateEpoch(teamName); + const requestedScope = captureTeamRequestScope(get, teamName); const existingRequest = inFlightTeamMessagesOlderRequests.get(teamName); if (existingRequest) { return existingRequest; @@ -2690,7 +2765,7 @@ export const createTeamSlice: StateCreator = (set, const existingHeadRequest = inFlightTeamMessagesHeadRequests.get(teamName); if (existingHeadRequest) { await existingHeadRequest; - if (!isTeamLocalStateEpochCurrent(teamName, requestedEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestedScope)) { return; } } @@ -2698,7 +2773,7 @@ export const createTeamSlice: StateCreator = (set, let entry = getTeamMessagesCacheEntry(get(), teamName); if (!entry.headHydrated) { await get().refreshTeamMessagesHead(teamName); - if (!isTeamLocalStateEpochCurrent(teamName, requestedEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestedScope)) { return; } entry = getTeamMessagesCacheEntry(get(), teamName); @@ -2710,7 +2785,7 @@ export const createTeamSlice: StateCreator = (set, const requestRef: { current: Promise | null } = { current: null }; requestRef.current = (async (): Promise => { - const teamStateEpoch = captureTeamLocalStateEpoch(teamName); + const requestScope = captureTeamRequestScope(get, teamName); set((state) => ({ teamMessagesByName: { ...state.teamMessagesByName, @@ -2729,7 +2804,7 @@ export const createTeamSlice: StateCreator = (set, limit: 50, }) ); - if (!isTeamLocalStateEpochCurrent(teamName, teamStateEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { return; } @@ -2780,7 +2855,7 @@ export const createTeamSlice: StateCreator = (set, }; }); } catch { - if (!isTeamLocalStateEpochCurrent(teamName, teamStateEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { return; } set((state) => ({ @@ -2818,12 +2893,12 @@ export const createTeamSlice: StateCreator = (set, const requestRef: { current: Promise | null } = { current: null }; requestRef.current = (async (): Promise => { - const teamStateEpoch = captureTeamLocalStateEpoch(teamName); + const requestScope = captureTeamRequestScope(get, teamName); try { const meta = await unwrapIpc('team:getMemberActivityMeta', () => api.teams.getMemberActivityMeta(teamName) ); - if (!isTeamLocalStateEpochCurrent(teamName, teamStateEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { return; } @@ -2857,14 +2932,17 @@ export const createTeamSlice: StateCreator = (set, }; }); } catch (error) { - if (!isTeamLocalStateEpochCurrent(teamName, teamStateEpoch)) { + if (!isTeamRequestScopeCurrent(get, teamName, requestScope)) { return; } throw error; } finally { if (inFlightTeamMemberActivityMetaRequests.get(teamName) === requestRef.current) { inFlightTeamMemberActivityMetaRequests.delete(teamName); - if (pendingFreshTeamMemberActivityMetaRefreshes.delete(teamName)) { + if ( + pendingFreshTeamMemberActivityMetaRefreshes.delete(teamName) && + isTeamRequestScopeCurrent(get, teamName, requestScope) + ) { void get().refreshMemberActivityMeta(teamName); } } diff --git a/src/renderer/store/utils/contextScopedRequestEpoch.ts b/src/renderer/store/utils/contextScopedRequestEpoch.ts new file mode 100644 index 00000000..7715e230 --- /dev/null +++ b/src/renderer/store/utils/contextScopedRequestEpoch.ts @@ -0,0 +1,17 @@ +let contextScopedRequestEpoch = 0; + +export function captureContextScopedRequestEpoch(): number { + return contextScopedRequestEpoch; +} + +export function isContextScopedRequestEpochCurrent(epoch: number): boolean { + return contextScopedRequestEpoch === epoch; +} + +export function invalidateContextScopedRequestEpoch(): void { + contextScopedRequestEpoch += 1; +} + +export function resetContextScopedRequestEpochForTests(): void { + contextScopedRequestEpoch = 0; +} diff --git a/test/renderer/store/contextSliceTeamReset.test.ts b/test/renderer/store/contextSliceTeamReset.test.ts index 6497d6a4..ee004f22 100644 --- a/test/renderer/store/contextSliceTeamReset.test.ts +++ b/test/renderer/store/contextSliceTeamReset.test.ts @@ -1,5 +1,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + invalidateContextScopedRequestEpoch, + resetContextScopedRequestEpochForTests, +} from '../../../src/renderer/store/utils/contextScopedRequestEpoch'; + import { createTestStore } from './storeTestUtils'; const apiMock = vi.hoisted(() => ({ @@ -119,6 +124,7 @@ function deferred(): { describe('context slice team/task reset', () => { beforeEach(() => { vi.clearAllMocks(); + resetContextScopedRequestEpochForTests(); contextStorageMock.loadSnapshot.mockResolvedValue(targetSnapshot()); apiMock.context.getActive.mockResolvedValue('local'); apiMock.getProjects.mockResolvedValue(targetSnapshot().projects); @@ -128,6 +134,7 @@ describe('context slice team/task reset', () => { }); afterEach(() => { + resetContextScopedRequestEpochForTests(); vi.restoreAllMocks(); }); @@ -225,6 +232,53 @@ describe('context slice team/task reset', () => { expect(store.getState().isContextSwitching).toBe(false); }); + it('does not apply a slow background project refresh after the context epoch changes again', async () => { + const projectScan = deferred(); + apiMock.getProjects.mockReturnValue(projectScan.promise); + apiMock.getRepositoryGroups.mockResolvedValue([]); + const store = createTestStore(); + const localProject = { + id: 'local-project', + name: 'Local Project', + path: '/local/project', + sessions: [], + createdAt: new Date(0).toISOString(), + updatedAt: new Date(0).toISOString(), + }; + + const switchPromise = store.getState().switchContext('ssh-dev'); + await Promise.resolve(); + await Promise.resolve(); + + expect(store.getState().activeContextId).toBe('ssh-dev'); + expect(store.getState().isContextSwitching).toBe(false); + + invalidateContextScopedRequestEpoch(); + store.setState({ + activeContextId: 'local', + projects: [localProject], + repositoryGroups: [], + isContextSwitching: false, + targetContextId: null, + } as never); + projectScan.resolve([ + { + id: 'late-ssh-project', + name: 'Late SSH Project', + path: '/ssh/late', + sessions: [], + createdAt: new Date(0).toISOString(), + updatedAt: new Date(0).toISOString(), + }, + ]); + await switchPromise; + + expect(store.getState().activeContextId).toBe('local'); + expect(store.getState().projects).toEqual([localProject]); + expect(apiMock.teams.list).not.toHaveBeenCalled(); + expect(apiMock.teams.getAllTasks).not.toHaveBeenCalled(); + }); + it('drops previous-context team and task caches when lazy context initialization changes context', async () => { apiMock.context.getActive.mockResolvedValue('ssh-dev'); const store = createTestStore(); diff --git a/test/renderer/store/teamSliceContextRace.test.ts b/test/renderer/store/teamSliceContextRace.test.ts index a9374ea5..6b430edd 100644 --- a/test/renderer/store/teamSliceContextRace.test.ts +++ b/test/renderer/store/teamSliceContextRace.test.ts @@ -2,9 +2,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { create } from 'zustand'; import { + __getTeamScopedTransientStateForTests, __resetTeamSliceModuleStateForTests, createTeamSlice, } from '../../../src/renderer/store/slices/teamSlice'; +import { invalidateTeamLocalStateEpoch } from '../../../src/renderer/store/team/teamLocalStateEpoch'; +import { invalidateContextScopedRequestEpoch } from '../../../src/renderer/store/utils/contextScopedRequestEpoch'; import type { AppState } from '../../../src/renderer/store/types'; @@ -12,8 +15,17 @@ const apiMock = vi.hoisted(() => ({ teams: { list: vi.fn(), getAllTasks: vi.fn(), + getData: vi.fn(), + getMessagesPage: vi.fn(), + getMemberActivityMeta: vi.fn(), + getMemberSpawnStatuses: vi.fn(), + getTeamAgentRuntime: vi.fn(), + getTaskChangePresence: vi.fn(), showMessageNotification: vi.fn(async () => undefined), }, + review: { + invalidateTaskChangeSummaries: vi.fn(async () => undefined), + }, })); interface TeamSummaryLike { @@ -32,6 +44,69 @@ interface GlobalTaskLike { comments: []; } +interface TeamSnapshotLike { + teamName: string; + config: { + name: string; + projectPath: string; + }; + tasks: Array<{ + id: string; + changePresence?: string; + }>; + members: []; + kanbanState: { + teamName: string; + reviewers: []; + tasks: Record; + }; + processes: []; +} + +const teamSnapshot = ( + teamName: string, + projectPath: string, + tasks: TeamSnapshotLike['tasks'] = [] +): TeamSnapshotLike => ({ + teamName, + config: { + name: teamName, + projectPath, + }, + tasks, + members: [], + kanbanState: { + teamName, + reviewers: [], + tasks: {}, + }, + processes: [], +}); + +const memberSpawnSnapshot = { + runId: 'runtime-run', + statuses: { + lead: { + status: 'online', + launchState: 'confirmed_alive', + }, + }, +}; + +const runtimeSnapshot = { + teamName: 'shared-team', + updatedAt: '2026-03-12T10:00:00.000Z', + runId: 'runtime-run', + members: { + lead: { + memberName: 'lead', + alive: true, + restartable: true, + updatedAt: '2026-03-12T10:00:00.000Z', + }, + }, +}; + vi.mock('@renderer/api', () => ({ api: apiMock, })); @@ -39,12 +114,21 @@ vi.mock('@renderer/api', () => ({ function deferred(): { promise: Promise; resolve: (value: T) => void; + reject: (reason?: unknown) => void; } { let resolve!: (value: T) => void; - const promise = new Promise((innerResolve) => { + let reject!: (reason?: unknown) => void; + const promise = new Promise((innerResolve, innerReject) => { resolve = innerResolve; + reject = innerReject; }); - return { promise, resolve }; + return { promise, resolve, reject }; +} + +async function flushMicrotasks(): Promise { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); } function createSliceStore() { @@ -70,6 +154,11 @@ function createSliceStore() { getAllPaneTabs: vi.fn(() => []), warmTaskChangeSummaries: vi.fn(async () => undefined), invalidateTaskChangePresence: vi.fn(), + projects: [], + repositoryGroups: [], + selectedProjectId: null, + selectedWorktreeId: null, + fetchSessionsInitial: vi.fn(async () => undefined), }) as unknown as AppState ); } @@ -79,7 +168,14 @@ describe('team slice context races', () => { __resetTeamSliceModuleStateForTests(); apiMock.teams.list.mockReset(); apiMock.teams.getAllTasks.mockReset(); + apiMock.teams.getData.mockReset(); + apiMock.teams.getMessagesPage.mockReset(); + apiMock.teams.getMemberActivityMeta.mockReset(); + apiMock.teams.getMemberSpawnStatuses.mockReset(); + apiMock.teams.getTeamAgentRuntime.mockReset(); + apiMock.teams.getTaskChangePresence.mockReset(); apiMock.teams.showMessageNotification.mockClear(); + apiMock.review.invalidateTaskChangeSummaries.mockClear(); }); afterEach(() => { @@ -116,6 +212,36 @@ describe('team slice context races', () => { expect(store.getState().teamsLoading).toBe(false); }); + it('ignores a team list response loaded before a context epoch reset with the same context id', async () => { + const store = createSliceStore(); + const localList = deferred(); + apiMock.teams.list.mockReturnValueOnce(localList.promise); + + const fetchPromise = store.getState().fetchTeams(); + expect(store.getState().teamsLoading).toBe(true); + + invalidateContextScopedRequestEpoch(); + store.setState({ + activeContextId: 'local', + teams: [], + teamByName: {}, + teamBySessionId: {}, + teamsLoading: false, + }); + localList.resolve([ + { + teamName: 'old-local-team', + displayName: 'Old Local Team', + projectPath: '/old-local/project', + }, + ]); + await fetchPromise; + + expect(store.getState().teams).toEqual([]); + expect(store.getState().teamByName).toEqual({}); + expect(store.getState().teamsLoading).toBe(false); + }); + it('reruns a pending global task refresh for the current context instead of applying stale tasks', async () => { const store = createSliceStore(); const localTasks = deferred(); @@ -163,4 +289,294 @@ describe('team slice context races', () => { expect(store.getState().globalTasksInitialized).toBe(true); expect(store.getState().globalTasksLoading).toBe(false); }); + + it('ignores global tasks loaded before a context epoch reset with the same context id', async () => { + const store = createSliceStore(); + const localTasks = deferred(); + apiMock.teams.getAllTasks.mockReturnValueOnce(localTasks.promise); + + const fetchPromise = store.getState().fetchAllTasks(); + expect(store.getState().globalTasksLoading).toBe(true); + + invalidateContextScopedRequestEpoch(); + store.setState({ + activeContextId: 'local', + globalTasks: [], + globalTasksLoading: false, + globalTasksInitialized: false, + }); + localTasks.resolve([ + { + id: 'old-local-task', + subject: 'Old local task', + status: 'todo', + teamName: 'old-local-team', + teamDisplayName: 'Old Local Team', + projectPath: '/old-local/project', + comments: [], + }, + ]); + await fetchPromise; + + expect(store.getState().globalTasks).toEqual([]); + expect(store.getState().globalTasksInitialized).toBe(false); + expect(store.getState().globalTasksLoading).toBe(false); + }); + + it('ignores selected team data loaded for a previous context', async () => { + const store = createSliceStore(); + const localData = deferred(); + apiMock.teams.getData.mockReturnValueOnce(localData.promise); + + const selectPromise = store.getState().selectTeam('shared-team'); + expect(store.getState().selectedTeamName).toBe('shared-team'); + + store.setState({ + activeContextId: 'ssh-dev', + selectedTeamName: null, + selectedTeamData: null, + selectedTeamLoading: false, + teamDataCacheByName: {}, + }); + localData.resolve(teamSnapshot('shared-team', '/local/project')); + await selectPromise; + + expect(store.getState().selectedTeamName).toBeNull(); + expect(store.getState().selectedTeamData).toBeNull(); + expect(store.getState().teamDataCacheByName).toEqual({}); + }); + + it('ignores selected team data loaded before a context epoch reset with the same context id', async () => { + const store = createSliceStore(); + const localData = deferred(); + apiMock.teams.getData.mockReturnValueOnce(localData.promise); + + const selectPromise = store.getState().selectTeam('shared-team'); + expect(store.getState().selectedTeamName).toBe('shared-team'); + + invalidateContextScopedRequestEpoch(); + store.setState({ + activeContextId: 'local', + selectedTeamName: null, + selectedTeamData: null, + selectedTeamLoading: false, + teamDataCacheByName: {}, + }); + localData.resolve(teamSnapshot('shared-team', '/old-local/project')); + await selectPromise; + + expect(store.getState().selectedTeamName).toBeNull(); + expect(store.getState().selectedTeamData).toBeNull(); + expect(store.getState().teamDataCacheByName).toEqual({}); + }); + + it('does not let a stale silent team refresh overwrite the current context cache', async () => { + const store = createSliceStore(); + const sshData = teamSnapshot('shared-team', '/ssh/project'); + const localData = deferred(); + apiMock.teams.getData.mockReturnValueOnce(localData.promise); + + const refreshPromise = store.getState().refreshTeamData('shared-team'); + store.setState({ + activeContextId: 'ssh-dev', + teamDataCacheByName: { + 'shared-team': sshData, + }, + } as never); + + localData.resolve(teamSnapshot('shared-team', '/local/project')); + await refreshPromise; + + expect(store.getState().teamDataCacheByName['shared-team']).toBe(sshData); + }); + + it('ignores message head pages loaded for a previous context', async () => { + const store = createSliceStore(); + const localMessages = deferred<{ + messages: []; + feedRevision: string; + nextCursor: null; + hasMore: false; + }>(); + apiMock.teams.getMessagesPage.mockReturnValueOnce(localMessages.promise); + + const refreshPromise = store.getState().refreshTeamMessagesHead('shared-team'); + expect(store.getState().teamMessagesByName['shared-team']).toBeDefined(); + + store.setState({ + activeContextId: 'ssh-dev', + teamMessagesByName: {}, + }); + localMessages.resolve({ + messages: [], + feedRevision: 'local-feed', + nextCursor: null, + hasMore: false, + }); + await refreshPromise; + + expect(store.getState().teamMessagesByName).toEqual({}); + }); + + it('ignores member spawn statuses loaded before a same-context team reset', async () => { + const store = createSliceStore(); + const localStatuses = deferred(); + apiMock.teams.getMemberSpawnStatuses.mockReturnValueOnce(localStatuses.promise); + + const refreshPromise = store.getState().fetchMemberSpawnStatuses('shared-team'); + invalidateTeamLocalStateEpoch('shared-team'); + localStatuses.resolve(memberSpawnSnapshot); + await refreshPromise; + + expect(store.getState().memberSpawnStatusesByTeam).toEqual({}); + expect(store.getState().memberSpawnSnapshotsByTeam).toEqual({}); + expect(store.getState().currentRuntimeRunIdByTeam).toEqual({}); + }); + + it('does not let stale member spawn IPC failures poison the next team scope', async () => { + const store = createSliceStore(); + const staleFailure = deferred(); + apiMock.teams.getMemberSpawnStatuses.mockReturnValueOnce(staleFailure.promise); + + const refreshPromise = store.getState().fetchMemberSpawnStatuses('shared-team'); + invalidateTeamLocalStateEpoch('shared-team'); + staleFailure.reject(new Error("No handler registered for 'team:memberSpawnStatuses'")); + await refreshPromise; + + expect( + __getTeamScopedTransientStateForTests('shared-team').hasMemberSpawnStatusesIpcBackoff + ).toBe(false); + }); + + it('ignores agent runtime snapshots loaded before a same-context team reset', async () => { + const store = createSliceStore(); + const localRuntime = deferred(); + apiMock.teams.getTeamAgentRuntime.mockReturnValueOnce(localRuntime.promise); + + const refreshPromise = store.getState().fetchTeamAgentRuntime('shared-team'); + invalidateTeamLocalStateEpoch('shared-team'); + localRuntime.resolve(runtimeSnapshot); + await refreshPromise; + + expect(store.getState().teamAgentRuntimeByTeam).toEqual({}); + }); + + it('ignores change presence loaded before a same-context team reset', async () => { + const store = createSliceStore(); + const staleData = teamSnapshot('shared-team', '/local/project', [ + { id: 'task-1', changePresence: 'unknown' }, + ]); + const localPresence = deferred<{ 'task-1': 'has_changes' }>(); + apiMock.teams.getTaskChangePresence.mockReturnValueOnce(localPresence.promise); + store.setState({ + selectedTeamName: 'shared-team', + selectedTeamData: staleData, + teamDataCacheByName: { + 'shared-team': staleData, + }, + } as never); + + const refreshPromise = store.getState().refreshTeamChangePresence('shared-team'); + invalidateTeamLocalStateEpoch('shared-team'); + localPresence.resolve({ 'task-1': 'has_changes' }); + await refreshPromise; + + expect(store.getState().selectedTeamData).toBe(staleData); + expect(store.getState().teamDataCacheByName['shared-team']).toBe(staleData); + }); + + it('does not rerun pending full team data refreshes from a stale scope', async () => { + const store = createSliceStore(); + const localData = deferred(); + apiMock.teams.getData + .mockReturnValueOnce(localData.promise) + .mockResolvedValueOnce(teamSnapshot('shared-team', '/unexpected/project')); + + const firstRefresh = store.getState().refreshTeamData('shared-team', { withDedup: true }); + const secondRefresh = store.getState().refreshTeamData('shared-team', { withDedup: true }); + invalidateTeamLocalStateEpoch('shared-team'); + store.setState({ teamDataCacheByName: {} }); + localData.resolve(teamSnapshot('shared-team', '/local/project')); + await Promise.all([firstRefresh, secondRefresh]); + await flushMicrotasks(); + + expect(apiMock.teams.getData).toHaveBeenCalledTimes(1); + }); + + it('does not rerun pending message head refreshes from a stale scope', async () => { + const store = createSliceStore(); + const localMessages = deferred<{ + messages: []; + feedRevision: string; + nextCursor: null; + hasMore: false; + }>(); + apiMock.teams.getMessagesPage.mockReturnValueOnce(localMessages.promise).mockResolvedValueOnce({ + messages: [], + feedRevision: 'unexpected-feed', + nextCursor: null, + hasMore: false, + }); + + const firstRefresh = store.getState().refreshTeamMessagesHead('shared-team'); + const secondRefresh = store.getState().refreshTeamMessagesHead('shared-team'); + invalidateTeamLocalStateEpoch('shared-team'); + store.setState({ teamMessagesByName: {} }); + localMessages.resolve({ + messages: [], + feedRevision: 'local-feed', + nextCursor: null, + hasMore: false, + }); + await Promise.all([firstRefresh, secondRefresh]); + await flushMicrotasks(); + + expect(apiMock.teams.getMessagesPage).toHaveBeenCalledTimes(1); + }); + + it('does not rerun pending member activity meta refreshes from a stale scope', async () => { + const store = createSliceStore(); + const localMeta = deferred<{ + teamName: string; + computedAt: string; + feedRevision: string; + members: Record; + }>(); + apiMock.teams.getMemberActivityMeta.mockReturnValueOnce(localMeta.promise).mockResolvedValueOnce({ + teamName: 'shared-team', + computedAt: '2026-03-12T10:00:01.000Z', + feedRevision: 'unexpected-feed', + members: {}, + }); + store.setState({ + teamMessagesByName: { + 'shared-team': { + canonicalMessages: [], + optimisticMessages: [], + feedRevision: 'feed-1', + nextCursor: null, + hasMore: false, + lastFetchedAt: 0, + loadingHead: false, + loadingOlder: false, + headHydrated: true, + }, + }, + } as never); + + const firstRefresh = store.getState().refreshMemberActivityMeta('shared-team'); + const secondRefresh = store.getState().refreshMemberActivityMeta('shared-team'); + invalidateTeamLocalStateEpoch('shared-team'); + store.setState({ teamMessagesByName: {}, memberActivityMetaByTeam: {} }); + localMeta.resolve({ + teamName: 'shared-team', + computedAt: '2026-03-12T10:00:00.000Z', + feedRevision: 'feed-1', + members: {}, + }); + await Promise.all([firstRefresh, secondRefresh]); + await flushMicrotasks(); + + expect(apiMock.teams.getMemberActivityMeta).toHaveBeenCalledTimes(1); + }); }); From d32db985b57bc90f4234677f71ba449de1e43237 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 17:42:12 +0300 Subject: [PATCH 13/59] fix(context): clear switch state on direct ssh reset --- src/renderer/store/slices/connectionSlice.ts | 4 ++++ test/renderer/store/contextSliceTeamReset.test.ts | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/renderer/store/slices/connectionSlice.ts b/src/renderer/store/slices/connectionSlice.ts index a59b23e7..7b551f0f 100644 --- a/src/renderer/store/slices/connectionSlice.ts +++ b/src/renderer/store/slices/connectionSlice.ts @@ -103,6 +103,8 @@ export const createConnectionSlice: StateCreator { }, ], globalTasksInitialized: true, + isContextSwitching: true, + targetContextId: 'local', } as never); await store.getState().connectSsh({ @@ -366,6 +368,8 @@ describe('context slice team/task reset', () => { expect(store.getState().teams).toEqual([]); expect(store.getState().teamByName).toEqual({}); expect(store.getState().globalTasks).toEqual([]); + expect(store.getState().isContextSwitching).toBe(false); + expect(store.getState().targetContextId).toBeNull(); expect(apiMock.teams.list).toHaveBeenCalledTimes(1); expect(apiMock.teams.getAllTasks).toHaveBeenCalledTimes(1); }); @@ -400,6 +404,8 @@ describe('context slice team/task reset', () => { }, ], globalTasksInitialized: true, + isContextSwitching: true, + targetContextId: 'local', } as never); await store.getState().disconnectSsh(); @@ -408,6 +414,8 @@ describe('context slice team/task reset', () => { expect(store.getState().teams).toEqual([]); expect(store.getState().teamByName).toEqual({}); expect(store.getState().globalTasks).toEqual([]); + expect(store.getState().isContextSwitching).toBe(false); + expect(store.getState().targetContextId).toBeNull(); expect(apiMock.teams.list).toHaveBeenCalledTimes(1); expect(apiMock.teams.getAllTasks).toHaveBeenCalledTimes(1); }); From 7514bf05eba3720a34c679ea0fea9525d6be8719 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 17:56:10 +0300 Subject: [PATCH 14/59] fix(recent-projects): guard context-scoped refreshes --- .../hooks/useRecentProjectsSection.ts | 30 +- .../hooks/useRecentProjectsSection.test.tsx | 263 ++++++++++++++++++ 2 files changed, 288 insertions(+), 5 deletions(-) create mode 100644 test/features/recent-projects/renderer/hooks/useRecentProjectsSection.test.tsx diff --git a/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts b/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts index 187851a1..6c56f77d 100644 --- a/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts +++ b/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts @@ -4,6 +4,10 @@ import { type DashboardRecentProject } from '@features/recent-projects/contracts import { api, isElectronMode } from '@renderer/api'; import { useStore } from '@renderer/store'; import { isTeamProvisioningActive } from '@renderer/store/slices/teamSlice'; +import { + captureContextScopedRequestEpoch, + isContextScopedRequestEpochCurrent, +} from '@renderer/store/utils/contextScopedRequestEpoch'; import { buildTaskCountsByProject } from '@renderer/utils/pathNormalize'; import { useShallow } from 'zustand/react/shallow'; @@ -134,6 +138,7 @@ export function useRecentProjectsSection( const reload = useCallback( async (options?: { force?: boolean }): Promise => { const requestContextId = activeContextId; + const requestContextEpoch = captureContextScopedRequestEpoch(); const hasVisibleProjects = recentProjectsRef.current.length > 0 || getRecentProjectsClientSnapshot(requestContextId) != null; @@ -148,19 +153,28 @@ export function useRecentProjectsSection( () => api.getDashboardRecentProjects(), options ); - if (activeContextIdRef.current !== requestContextId) { + if ( + activeContextIdRef.current !== requestContextId || + !isContextScopedRequestEpochCurrent(requestContextEpoch) + ) { return; } setRecentProjects(payload.projects); setRecentProjectsDegraded(payload.degraded); setDegradedRefreshCount((current) => (payload.degraded ? current + 1 : 0)); } catch (nextError) { - if (activeContextIdRef.current !== requestContextId) { + if ( + activeContextIdRef.current !== requestContextId || + !isContextScopedRequestEpochCurrent(requestContextEpoch) + ) { return; } setError(nextError instanceof Error ? nextError.message : 'Failed to load recent projects'); } finally { - if (activeContextIdRef.current === requestContextId) { + if ( + activeContextIdRef.current === requestContextId && + isContextScopedRequestEpochCurrent(requestContextEpoch) + ) { setLoading(false); } } @@ -220,11 +234,17 @@ export function useRecentProjectsSection( useEffect(() => { let cancelled = false; + const requestContextId = activeContextId; + const requestContextEpoch = captureContextScopedRequestEpoch(); void api.teams .aliveList() .then((teamNames) => { - if (!cancelled) { + if ( + !cancelled && + activeContextIdRef.current === requestContextId && + isContextScopedRequestEpochCurrent(requestContextEpoch) + ) { setAliveTeams(teamNames); } }) @@ -233,7 +253,7 @@ export function useRecentProjectsSection( return () => { cancelled = true; }; - }, [provisioningTeamNamesKey, teams]); + }, [activeContextId, provisioningTeamNamesKey, teams]); useEffect(() => { if (!searchQuery.trim()) { diff --git a/test/features/recent-projects/renderer/hooks/useRecentProjectsSection.test.tsx b/test/features/recent-projects/renderer/hooks/useRecentProjectsSection.test.tsx new file mode 100644 index 00000000..1f07d9a5 --- /dev/null +++ b/test/features/recent-projects/renderer/hooks/useRecentProjectsSection.test.tsx @@ -0,0 +1,263 @@ +import React, { act } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; + +import { useRecentProjectsSection } from '@features/recent-projects/renderer/hooks/useRecentProjectsSection'; +import { + __resetRecentProjectsClientCacheForTests, + loadRecentProjectsWithClientCache, +} from '@features/recent-projects/renderer/utils/recentProjectsClientCache'; +import { + invalidateContextScopedRequestEpoch, + resetContextScopedRequestEpochForTests, +} from '@renderer/store/utils/contextScopedRequestEpoch'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { + DashboardRecentProject, + DashboardRecentProjectsPayload, +} from '@features/recent-projects/contracts'; +import type { TeamSummary } from '@shared/types'; + +(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = + true; + +const apiMock = vi.hoisted(() => ({ + getDashboardRecentProjects: vi.fn(), + teams: { + aliveList: vi.fn(), + }, + config: { + addCustomProjectPath: vi.fn(), + selectFolders: vi.fn(), + }, + openPath: vi.fn(), +})); + +const storeState = vi.hoisted(() => ({ + globalTasks: [], + globalTasksInitialized: false, + globalTasksLoading: false, + fetchAllTasks: vi.fn(), + teams: [] as TeamSummary[], + activeContextId: 'local', + provisioningRuns: {}, + currentProvisioningRunIdByTeam: {}, + provisioningSnapshotByTeam: {}, + repositoryGroups: [], + fetchRepositoryGroups: vi.fn(), + openTeamsTab: vi.fn(), + fetchSessionsInitial: vi.fn(), +})); + +vi.mock('@renderer/api', () => ({ + api: apiMock, + isElectronMode: () => true, +})); + +vi.mock('@renderer/store', () => { + const useStore = Object.assign( + (selector: (state: typeof storeState) => unknown) => selector(storeState), + { + getState: () => storeState, + setState: vi.fn((patch: Partial) => { + Object.assign(storeState, patch); + }), + } + ); + return { useStore }; +}); + +vi.mock('zustand/react/shallow', () => ({ + useShallow: (selector: T) => selector, +})); + +function deferred(): { + promise: Promise; + resolve: (value: T) => void; +} { + let resolve!: (value: T) => void; + const promise = new Promise((promiseResolve) => { + resolve = promiseResolve; + }); + return { promise, resolve }; +} + +function project(id: string, projectPath = `/tmp/${id}`): DashboardRecentProject { + return { + id, + name: id, + primaryPath: projectPath, + associatedPaths: [projectPath], + mostRecentActivity: Date.parse('2026-04-14T12:00:00.000Z'), + providerIds: ['anthropic'], + source: 'claude', + openTarget: { + type: 'synthetic-path', + path: projectPath, + }, + }; +} + +function payload(id: string, projectPath?: string): DashboardRecentProjectsPayload { + return { + projects: [project(id, projectPath)], + degraded: false, + }; +} + +function team(teamName: string, projectPath: string): TeamSummary { + return { + teamName, + displayName: teamName, + description: '', + memberCount: 1, + taskCount: 0, + lastActivity: null, + projectPath, + }; +} + +async function flushPromises(): Promise { + await Promise.resolve(); + await Promise.resolve(); +} + +describe('useRecentProjectsSection', () => { + let host: HTMLDivElement; + let root: Root; + let latest: ReturnType | null; + + function Harness(): React.JSX.Element | null { + latest = useRecentProjectsSection('', 20); + return null; + } + + async function renderHarness(): Promise { + await act(async () => { + root.render(React.createElement(Harness)); + await flushPromises(); + }); + } + + beforeEach(() => { + __resetRecentProjectsClientCacheForTests(); + resetContextScopedRequestEpochForTests(); + vi.clearAllMocks(); + latest = null; + storeState.globalTasks = []; + storeState.globalTasksInitialized = false; + storeState.globalTasksLoading = false; + storeState.teams = []; + storeState.activeContextId = 'local'; + storeState.provisioningRuns = {}; + storeState.currentProvisioningRunIdByTeam = {}; + storeState.provisioningSnapshotByTeam = {}; + storeState.repositoryGroups = []; + apiMock.teams.aliveList.mockResolvedValue([]); + + host = document.createElement('div'); + document.body.appendChild(host); + root = createRoot(host); + }); + + afterEach(() => { + act(() => { + root.unmount(); + }); + host.remove(); + __resetRecentProjectsClientCacheForTests(); + resetContextScopedRequestEpochForTests(); + }); + + it('ignores stale recent-project loads after the context epoch changes back to the same id', async () => { + const oldLocalRequest = deferred(); + const sshRequest = deferred(); + const freshLocalRequest = deferred(); + + apiMock.getDashboardRecentProjects + .mockReturnValueOnce(oldLocalRequest.promise) + .mockReturnValueOnce(sshRequest.promise) + .mockReturnValueOnce(freshLocalRequest.promise); + + await renderHarness(); + expect(apiMock.getDashboardRecentProjects).toHaveBeenCalledTimes(1); + + invalidateContextScopedRequestEpoch(); + storeState.activeContextId = 'ssh-dev'; + await renderHarness(); + expect(apiMock.getDashboardRecentProjects).toHaveBeenCalledTimes(2); + + invalidateContextScopedRequestEpoch(); + storeState.activeContextId = 'local'; + await renderHarness(); + expect(apiMock.getDashboardRecentProjects).toHaveBeenCalledTimes(3); + + await act(async () => { + oldLocalRequest.resolve(payload('old-local')); + await oldLocalRequest.promise; + await flushPromises(); + }); + + expect(latest?.cards.map((card) => card.name)).toEqual([]); + expect(latest?.loading).toBe(true); + + await act(async () => { + freshLocalRequest.resolve(payload('fresh-local')); + await freshLocalRequest.promise; + await flushPromises(); + }); + + expect(latest?.cards.map((card) => card.name)).toEqual(['fresh-local']); + expect(latest?.loading).toBe(false); + }); + + it('cancels stale alive-list responses when only the active context changes', async () => { + const oldAliveRequest = deferred(); + const sshAliveRequest = deferred(); + const freshAliveRequest = deferred(); + + await loadRecentProjectsWithClientCache('local', () => Promise.resolve(payload('alpha')), { + force: true, + }); + + apiMock.getDashboardRecentProjects.mockResolvedValue(payload('alpha')); + apiMock.teams.aliveList + .mockReturnValueOnce(oldAliveRequest.promise) + .mockReturnValueOnce(sshAliveRequest.promise) + .mockReturnValueOnce(freshAliveRequest.promise); + storeState.teams = [team('old-team', '/tmp/alpha'), team('fresh-team', '/tmp/alpha')]; + + await renderHarness(); + expect(apiMock.teams.aliveList).toHaveBeenCalledTimes(1); + + invalidateContextScopedRequestEpoch(); + storeState.activeContextId = 'ssh-dev'; + await renderHarness(); + expect(apiMock.teams.aliveList).toHaveBeenCalledTimes(2); + + invalidateContextScopedRequestEpoch(); + storeState.activeContextId = 'local'; + await renderHarness(); + expect(apiMock.teams.aliveList).toHaveBeenCalledTimes(3); + + await act(async () => { + freshAliveRequest.resolve(['fresh-team']); + await freshAliveRequest.promise; + await flushPromises(); + }); + + expect(latest?.cards[0]?.activeTeams?.map((activeTeam) => activeTeam.teamName)).toEqual([ + 'fresh-team', + ]); + + await act(async () => { + oldAliveRequest.resolve(['old-team']); + await oldAliveRequest.promise; + await flushPromises(); + }); + + expect(latest?.cards[0]?.activeTeams?.map((activeTeam) => activeTeam.teamName)).toEqual([ + 'fresh-team', + ]); + }); +}); From 636d121f5f6955c3cf6bd0250b477490256a1727 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 18:21:23 +0300 Subject: [PATCH 15/59] fix(team): guard cross-team targets by context --- src/renderer/store/slices/teamSlice.ts | 7 +++ .../store/teamSliceContextRace.test.ts | 60 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 423665fc..23063056 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -3133,11 +3133,18 @@ export const createTeamSlice: StateCreator = (set, }, fetchCrossTeamTargets: async () => { + const requestScope = captureContextRequestScope(get); set({ crossTeamTargetsLoading: true }); try { const targets = await api.crossTeam.listTargets(); + if (!isContextRequestScopeCurrent(get, requestScope)) { + return; + } set({ crossTeamTargets: targets, crossTeamTargetsLoading: false }); } catch (error) { + if (!isContextRequestScopeCurrent(get, requestScope)) { + return; + } logger.error('fetchCrossTeamTargets failed', error); set({ crossTeamTargets: [], crossTeamTargetsLoading: false }); } diff --git a/test/renderer/store/teamSliceContextRace.test.ts b/test/renderer/store/teamSliceContextRace.test.ts index 6b430edd..c06aa7c2 100644 --- a/test/renderer/store/teamSliceContextRace.test.ts +++ b/test/renderer/store/teamSliceContextRace.test.ts @@ -26,6 +26,9 @@ const apiMock = vi.hoisted(() => ({ review: { invalidateTaskChangeSummaries: vi.fn(async () => undefined), }, + crossTeam: { + listTargets: vi.fn(), + }, })); interface TeamSummaryLike { @@ -63,6 +66,11 @@ interface TeamSnapshotLike { processes: []; } +interface CrossTeamTargetLike { + teamName: string; + displayName: string; +} + const teamSnapshot = ( teamName: string, projectPath: string, @@ -176,6 +184,7 @@ describe('team slice context races', () => { apiMock.teams.getTaskChangePresence.mockReset(); apiMock.teams.showMessageNotification.mockClear(); apiMock.review.invalidateTaskChangeSummaries.mockClear(); + apiMock.crossTeam.listTargets.mockReset(); }); afterEach(() => { @@ -323,6 +332,57 @@ describe('team slice context races', () => { expect(store.getState().globalTasksLoading).toBe(false); }); + it('ignores cross-team targets loaded for a previous context', async () => { + const store = createSliceStore(); + const localTargets = deferred(); + apiMock.crossTeam.listTargets.mockReturnValueOnce(localTargets.promise); + + const fetchPromise = store.getState().fetchCrossTeamTargets(); + expect(store.getState().crossTeamTargetsLoading).toBe(true); + + store.setState({ + activeContextId: 'ssh-dev', + crossTeamTargets: [], + crossTeamTargetsLoading: false, + }); + localTargets.resolve([ + { + teamName: 'local-target', + displayName: 'Local Target', + }, + ]); + await fetchPromise; + + expect(store.getState().crossTeamTargets).toEqual([]); + expect(store.getState().crossTeamTargetsLoading).toBe(false); + }); + + it('ignores cross-team targets loaded before a context epoch reset with the same context id', async () => { + const store = createSliceStore(); + const localTargets = deferred(); + apiMock.crossTeam.listTargets.mockReturnValueOnce(localTargets.promise); + + const fetchPromise = store.getState().fetchCrossTeamTargets(); + expect(store.getState().crossTeamTargetsLoading).toBe(true); + + invalidateContextScopedRequestEpoch(); + store.setState({ + activeContextId: 'local', + crossTeamTargets: [], + crossTeamTargetsLoading: false, + }); + localTargets.resolve([ + { + teamName: 'old-local-target', + displayName: 'Old Local Target', + }, + ]); + await fetchPromise; + + expect(store.getState().crossTeamTargets).toEqual([]); + expect(store.getState().crossTeamTargetsLoading).toBe(false); + }); + it('ignores selected team data loaded for a previous context', async () => { const store = createSliceStore(); const localData = deferred(); From 2fdbf301b4d8cb144de6537c424a1205f5562911 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 18:33:47 +0300 Subject: [PATCH 16/59] fix(context): guard project fetches by scope --- src/renderer/store/slices/projectSlice.ts | 18 ++ src/renderer/store/slices/repositorySlice.ts | 18 ++ .../projectRepositoryContextRace.test.ts | 204 ++++++++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 test/renderer/store/projectRepositoryContextRace.test.ts diff --git a/src/renderer/store/slices/projectSlice.ts b/src/renderer/store/slices/projectSlice.ts index 6e74b25d..09bccd6e 100644 --- a/src/renderer/store/slices/projectSlice.ts +++ b/src/renderer/store/slices/projectSlice.ts @@ -4,6 +4,10 @@ import { api } from '@renderer/api'; +import { + captureContextScopedRequestEpoch, + isContextScopedRequestEpochCurrent, +} from '../utils/contextScopedRequestEpoch'; import { getSessionResetState } from '../utils/stateResetHelpers'; import type { AppState } from '../types'; @@ -43,15 +47,29 @@ export const createProjectSlice: StateCreator = fetchProjects: async () => { // Guard: prevent concurrent fetches (component mount + centralized init chain) if (get().projectsLoading) return; + const requestContextId = get().activeContextId; + const requestContextEpoch = captureContextScopedRequestEpoch(); set({ projectsLoading: true, projectsError: null }); try { const projects = await api.getProjects(); + if ( + get().activeContextId !== requestContextId || + !isContextScopedRequestEpochCurrent(requestContextEpoch) + ) { + return; + } // Sort by most recent session (descending) const sorted = [...projects].sort( (a, b) => (b.mostRecentSession ?? 0) - (a.mostRecentSession ?? 0) ); set({ projects: sorted, projectsLoading: false, projectsInitialized: true }); } catch (error) { + if ( + get().activeContextId !== requestContextId || + !isContextScopedRequestEpochCurrent(requestContextEpoch) + ) { + return; + } set({ projectsError: error instanceof Error ? error.message : 'Failed to fetch projects', projectsLoading: false, diff --git a/src/renderer/store/slices/repositorySlice.ts b/src/renderer/store/slices/repositorySlice.ts index 0d439c68..425f708e 100644 --- a/src/renderer/store/slices/repositorySlice.ts +++ b/src/renderer/store/slices/repositorySlice.ts @@ -5,6 +5,10 @@ import { api } from '@renderer/api'; import { createLogger } from '@shared/utils/logger'; +import { + captureContextScopedRequestEpoch, + isContextScopedRequestEpochCurrent, +} from '../utils/contextScopedRequestEpoch'; import { getSessionResetState } from '../utils/stateResetHelpers'; import type { AppState } from '../types'; @@ -71,6 +75,8 @@ export const createRepositorySlice: StateCreator { // Guard: prevent concurrent fetches (component mount + centralized init chain) if (get().repositoryGroupsLoading) return; + const requestContextId = get().activeContextId; + const requestContextEpoch = captureContextScopedRequestEpoch(); const startedAt = Date.now(); set({ repositoryGroupsLoading: true, repositoryGroupsError: null }); try { @@ -79,6 +85,12 @@ export const createRepositorySlice: StateCreator ({ + getProjects: vi.fn(), + getRepositoryGroups: vi.fn(), +})); + +vi.mock('@renderer/api', () => ({ + api: apiMock, +})); + +function deferred(): { + promise: Promise; + resolve: (value: T) => void; +} { + let resolve!: (value: T) => void; + const promise = new Promise((innerResolve) => { + resolve = innerResolve; + }); + return { promise, resolve }; +} + +function project(id: string, path = `/${id}`): Project { + return { + id, + path, + name: id, + sessions: [], + totalSessions: 0, + createdAt: 0, + mostRecentSession: 0, + }; +} + +function repositoryGroup(id: string, path = `/${id}`): RepositoryGroup { + return { + id, + identity: null, + name: id, + totalSessions: 0, + mostRecentSession: 0, + worktrees: [ + { + id: `${id}-worktree`, + path, + name: id, + isMainWorktree: true, + source: 'unknown', + sessions: [], + totalSessions: 0, + createdAt: 0, + mostRecentSession: 0, + }, + ], + }; +} + +function createProjectRepositoryStore() { + return create()((set, get, store) => + ({ + ...createProjectSlice(set as never, get as never, store as never), + ...createRepositorySlice(set as never, get as never, store as never), + activeContextId: 'local', + activeProjectId: null, + fetchSessionsInitial: vi.fn(async () => undefined), + }) as unknown as AppState + ); +} + +describe('project and repository context races', () => { + beforeEach(() => { + resetContextScopedRequestEpochForTests(); + apiMock.getProjects.mockReset(); + apiMock.getRepositoryGroups.mockReset(); + }); + + afterEach(() => { + resetContextScopedRequestEpochForTests(); + vi.restoreAllMocks(); + }); + + it('applies current-context project loads', async () => { + const store = createProjectRepositoryStore(); + apiMock.getProjects.mockResolvedValue([project('current-project')]); + + await store.getState().fetchProjects(); + + expect(store.getState().projects).toEqual([project('current-project')]); + expect(store.getState().projectsInitialized).toBe(true); + expect(store.getState().projectsLoading).toBe(false); + }); + + it('ignores project loads resolved for a previous context', async () => { + const store = createProjectRepositoryStore(); + const localProjects = deferred(); + const currentProjects = [project('ssh-project', '/ssh/project')]; + apiMock.getProjects.mockReturnValueOnce(localProjects.promise); + + const fetchPromise = store.getState().fetchProjects(); + expect(store.getState().projectsLoading).toBe(true); + + store.setState({ + activeContextId: 'ssh-dev', + projects: currentProjects, + projectsLoading: false, + projectsInitialized: true, + }); + localProjects.resolve([project('local-project', '/local/project')]); + await fetchPromise; + + expect(store.getState().projects).toBe(currentProjects); + expect(store.getState().projectsLoading).toBe(false); + }); + + it('ignores project loads resolved before a same-context epoch reset', async () => { + const store = createProjectRepositoryStore(); + const oldLocalProjects = deferred(); + const currentProjects = [project('fresh-local-project', '/fresh-local/project')]; + apiMock.getProjects.mockReturnValueOnce(oldLocalProjects.promise); + + const fetchPromise = store.getState().fetchProjects(); + expect(store.getState().projectsLoading).toBe(true); + + invalidateContextScopedRequestEpoch(); + store.setState({ + activeContextId: 'local', + projects: currentProjects, + projectsLoading: false, + projectsInitialized: true, + }); + oldLocalProjects.resolve([project('old-local-project', '/old-local/project')]); + await fetchPromise; + + expect(store.getState().projects).toBe(currentProjects); + expect(store.getState().projectsLoading).toBe(false); + }); + + it('applies current-context repository group loads', async () => { + const store = createProjectRepositoryStore(); + apiMock.getRepositoryGroups.mockResolvedValue([repositoryGroup('current-repo')]); + + await store.getState().fetchRepositoryGroups(); + + expect(store.getState().repositoryGroups).toEqual([repositoryGroup('current-repo')]); + expect(store.getState().repositoryGroupsInitialized).toBe(true); + expect(store.getState().repositoryGroupsLoading).toBe(false); + }); + + it('ignores repository group loads resolved for a previous context', async () => { + const store = createProjectRepositoryStore(); + const localGroups = deferred(); + const currentGroups = [repositoryGroup('ssh-repo', '/ssh/repo')]; + apiMock.getRepositoryGroups.mockReturnValueOnce(localGroups.promise); + + const fetchPromise = store.getState().fetchRepositoryGroups(); + expect(store.getState().repositoryGroupsLoading).toBe(true); + + store.setState({ + activeContextId: 'ssh-dev', + repositoryGroups: currentGroups, + repositoryGroupsLoading: false, + repositoryGroupsInitialized: true, + }); + localGroups.resolve([repositoryGroup('local-repo', '/local/repo')]); + await fetchPromise; + + expect(store.getState().repositoryGroups).toBe(currentGroups); + expect(store.getState().repositoryGroupsLoading).toBe(false); + }); + + it('ignores repository group loads resolved before a same-context epoch reset', async () => { + const store = createProjectRepositoryStore(); + const oldLocalGroups = deferred(); + const currentGroups = [repositoryGroup('fresh-local-repo', '/fresh-local/repo')]; + apiMock.getRepositoryGroups.mockReturnValueOnce(oldLocalGroups.promise); + + const fetchPromise = store.getState().fetchRepositoryGroups(); + expect(store.getState().repositoryGroupsLoading).toBe(true); + + invalidateContextScopedRequestEpoch(); + store.setState({ + activeContextId: 'local', + repositoryGroups: currentGroups, + repositoryGroupsLoading: false, + repositoryGroupsInitialized: true, + }); + oldLocalGroups.resolve([repositoryGroup('old-local-repo', '/old-local/repo')]); + await fetchPromise; + + expect(store.getState().repositoryGroups).toBe(currentGroups); + expect(store.getState().repositoryGroupsLoading).toBe(false); + }); +}); From 1b36d1daa683290c7a252e3b1b67f14c74a8ecf8 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 18:47:16 +0300 Subject: [PATCH 17/59] fix(context): clear project loading on ssh reset --- src/renderer/store/slices/connectionSlice.ts | 12 ++++ .../store/contextSliceTeamReset.test.ts | 68 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/renderer/store/slices/connectionSlice.ts b/src/renderer/store/slices/connectionSlice.ts index 7b551f0f..461a1cad 100644 --- a/src/renderer/store/slices/connectionSlice.ts +++ b/src/renderer/store/slices/connectionSlice.ts @@ -85,7 +85,13 @@ export const createConnectionSlice: StateCreator(): { return { promise, resolve }; } +async function flushMicrotasks(): Promise { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); +} + describe('context slice team/task reset', () => { beforeEach(() => { vi.clearAllMocks(); @@ -374,6 +380,40 @@ describe('context slice team/task reset', () => { expect(apiMock.teams.getAllTasks).toHaveBeenCalledTimes(1); }); + it('clears project and repository loading guards before direct SSH connect refetches', async () => { + const projectScan = deferred(); + const repositoryScan = deferred(); + apiMock.getProjects.mockReturnValue(projectScan.promise); + apiMock.getRepositoryGroups.mockReturnValue(repositoryScan.promise); + const store = createTestStore(); + store.setState({ + activeContextId: 'local', + projectsLoading: true, + repositoryGroupsLoading: true, + } as never); + + await store.getState().connectSsh({ + host: 'dev', + port: 22, + username: 'me', + authMethod: 'privateKey', + privateKeyPath: '/tmp/key', + }); + + expect(apiMock.getProjects).toHaveBeenCalledTimes(1); + expect(apiMock.getRepositoryGroups).toHaveBeenCalledTimes(1); + expect(store.getState().projectsLoading).toBe(true); + expect(store.getState().repositoryGroupsLoading).toBe(true); + + projectScan.resolve([]); + repositoryScan.resolve([]); + await Promise.all([projectScan.promise, repositoryScan.promise]); + await flushMicrotasks(); + + expect(store.getState().projectsLoading).toBe(false); + expect(store.getState().repositoryGroupsLoading).toBe(false); + }); + it('drops previous-context team and task caches on direct SSH disconnect', async () => { const store = createTestStore(); store.setState({ @@ -419,4 +459,32 @@ describe('context slice team/task reset', () => { expect(apiMock.teams.list).toHaveBeenCalledTimes(1); expect(apiMock.teams.getAllTasks).toHaveBeenCalledTimes(1); }); + + it('clears project and repository loading guards before direct SSH disconnect refetches', async () => { + const projectScan = deferred(); + const repositoryScan = deferred(); + apiMock.getProjects.mockReturnValue(projectScan.promise); + apiMock.getRepositoryGroups.mockReturnValue(repositoryScan.promise); + const store = createTestStore(); + store.setState({ + activeContextId: 'ssh-dev', + projectsLoading: true, + repositoryGroupsLoading: true, + } as never); + + await store.getState().disconnectSsh(); + + expect(apiMock.getProjects).toHaveBeenCalledTimes(1); + expect(apiMock.getRepositoryGroups).toHaveBeenCalledTimes(1); + expect(store.getState().projectsLoading).toBe(true); + expect(store.getState().repositoryGroupsLoading).toBe(true); + + projectScan.resolve([]); + repositoryScan.resolve([]); + await Promise.all([projectScan.promise, repositoryScan.promise]); + await flushMicrotasks(); + + expect(store.getState().projectsLoading).toBe(false); + expect(store.getState().repositoryGroupsLoading).toBe(false); + }); }); From 1eae8305ea6e3ff49470e3d205147a3880852e55 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 18:54:17 +0300 Subject: [PATCH 18/59] fix(context): reset lazy project scope --- src/renderer/store/slices/contextSlice.ts | 17 +++++- .../store/contextSliceTeamReset.test.ts | 56 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/renderer/store/slices/contextSlice.ts b/src/renderer/store/slices/contextSlice.ts index 4b016d27..f251f881 100644 --- a/src/renderer/store/slices/contextSlice.ts +++ b/src/renderer/store/slices/contextSlice.ts @@ -282,11 +282,26 @@ export const createContextSlice: StateCreator = } set({ - ...(contextChanged ? getContextScopedTeamResetState() : {}), + ...(contextChanged + ? { + ...getFullResetState(), + ...getContextScopedTeamResetState(), + projects: [], + projectsLoading: false, + projectsInitialized: false, + projectsError: null, + repositoryGroups: [], + repositoryGroupsLoading: false, + repositoryGroupsInitialized: false, + repositoryGroupsError: null, + } + : {}), contextSnapshotsReady: true, activeContextId, }); if (contextChanged) { + void get().fetchProjects(); + void get().fetchRepositoryGroups(); void get().fetchTeams(); void get().fetchAllTasks(); } diff --git a/test/renderer/store/contextSliceTeamReset.test.ts b/test/renderer/store/contextSliceTeamReset.test.ts index e13e9c59..ab08dd59 100644 --- a/test/renderer/store/contextSliceTeamReset.test.ts +++ b/test/renderer/store/contextSliceTeamReset.test.ts @@ -290,6 +290,27 @@ describe('context slice team/task reset', () => { const store = createTestStore(); store.setState({ activeContextId: 'local', + projects: [ + { + id: 'local-project', + name: 'Local Project', + path: '/local/project', + sessions: [], + createdAt: new Date(0).toISOString(), + updatedAt: new Date(0).toISOString(), + }, + ], + projectsInitialized: true, + repositoryGroups: [ + { + id: 'local-repo', + identity: null, + name: 'Local Repo', + totalSessions: 0, + worktrees: [], + }, + ], + repositoryGroupsInitialized: true, teams: [ { teamName: 'local-team', @@ -321,13 +342,48 @@ describe('context slice team/task reset', () => { await store.getState().initializeContextSystem(); expect(store.getState().activeContextId).toBe('ssh-dev'); + expect(store.getState().projects).toEqual(targetSnapshot().projects); + expect(store.getState().projectsInitialized).toBe(true); + expect(store.getState().repositoryGroups).toEqual([]); + expect(store.getState().repositoryGroupsInitialized).toBe(true); expect(store.getState().teams).toEqual([]); expect(store.getState().teamByName).toEqual({}); expect(store.getState().globalTasks).toEqual([]); + expect(apiMock.getProjects).toHaveBeenCalledTimes(1); + expect(apiMock.getRepositoryGroups).toHaveBeenCalledTimes(1); expect(apiMock.teams.list).toHaveBeenCalledTimes(1); expect(apiMock.teams.getAllTasks).toHaveBeenCalledTimes(1); }); + it('clears project and repository loading guards before lazy context initialization refetches', async () => { + apiMock.context.getActive.mockResolvedValue('ssh-dev'); + const projectScan = deferred(); + const repositoryScan = deferred(); + apiMock.getProjects.mockReturnValue(projectScan.promise); + apiMock.getRepositoryGroups.mockReturnValue(repositoryScan.promise); + const store = createTestStore(); + store.setState({ + activeContextId: 'local', + projectsLoading: true, + repositoryGroupsLoading: true, + } as never); + + await store.getState().initializeContextSystem(); + + expect(apiMock.getProjects).toHaveBeenCalledTimes(1); + expect(apiMock.getRepositoryGroups).toHaveBeenCalledTimes(1); + expect(store.getState().projectsLoading).toBe(true); + expect(store.getState().repositoryGroupsLoading).toBe(true); + + projectScan.resolve([]); + repositoryScan.resolve([]); + await Promise.all([projectScan.promise, repositoryScan.promise]); + await flushMicrotasks(); + + expect(store.getState().projectsLoading).toBe(false); + expect(store.getState().repositoryGroupsLoading).toBe(false); + }); + it('drops previous-context team and task caches on direct SSH connect', async () => { const store = createTestStore(); store.setState({ From 5355570f2c8b4104e27613cf9f786dfe3a565801 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 19:01:43 +0300 Subject: [PATCH 19/59] test(context): cover unchanged lazy init --- .../store/contextSliceTeamReset.test.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/renderer/store/contextSliceTeamReset.test.ts b/test/renderer/store/contextSliceTeamReset.test.ts index ab08dd59..fb8c22da 100644 --- a/test/renderer/store/contextSliceTeamReset.test.ts +++ b/test/renderer/store/contextSliceTeamReset.test.ts @@ -144,6 +144,64 @@ describe('context slice team/task reset', () => { vi.restoreAllMocks(); }); + it('does not refetch context-scoped data when lazy initialization keeps the same context', async () => { + const store = createTestStore(); + store.setState({ + activeContextId: 'local', + projects: [ + { + id: 'local-project', + name: 'Local Project', + path: '/local/project', + sessions: [], + createdAt: new Date(0).toISOString(), + updatedAt: new Date(0).toISOString(), + }, + ], + projectsInitialized: true, + repositoryGroups: [ + { + id: 'local-repo', + identity: null, + name: 'Local Repo', + totalSessions: 0, + worktrees: [], + }, + ], + repositoryGroupsInitialized: true, + teams: [ + { + teamName: 'local-team', + displayName: 'Local Team', + projectPath: '/local/project', + }, + ], + globalTasks: [ + { + id: 'local-task', + subject: 'Local task', + status: 'todo', + teamName: 'local-team', + teamDisplayName: 'Local Team', + projectPath: '/local/project', + comments: [], + }, + ], + globalTasksInitialized: true, + } as never); + + await store.getState().initializeContextSystem(); + + expect(store.getState().activeContextId).toBe('local'); + expect(store.getState().projectsInitialized).toBe(true); + expect(store.getState().repositoryGroupsInitialized).toBe(true); + expect(apiMock.context.list).toHaveBeenCalledTimes(1); + expect(apiMock.getProjects).not.toHaveBeenCalled(); + expect(apiMock.getRepositoryGroups).not.toHaveBeenCalled(); + expect(apiMock.teams.list).not.toHaveBeenCalled(); + expect(apiMock.teams.getAllTasks).not.toHaveBeenCalled(); + }); + it('drops previous-context team and task caches before refreshing the target context', async () => { const store = createTestStore(); store.setState({ From 58a0eb603ddf2ac17131d07f3a64720c7ab1e91f Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 19:44:23 +0300 Subject: [PATCH 20/59] build(runtime): require Node 24 toolchain --- .dockerignore | 52 ++ .github/workflows/ci.yml | 6 +- .github/workflows/codex-runtime-smoke.yml | 2 +- .github/workflows/landing.yml | 2 +- .github/workflows/release.yml | 8 +- .node-version | 1 + .npmrc | 1 + .nvmrc | 1 + agent-teams-controller/package.json | 2 +- docker/Dockerfile | 42 +- docker/vite.standalone.config.ts | 10 +- electron.vite.config.ts | 6 +- landing/.npmrc | 1 + landing/package-lock.json | 3 + landing/package.json | 3 + mcp-server/package.json | 6 +- mcp-server/tsup.config.ts | 2 +- package.json | 9 +- packages/agent-graph/package.json | 3 + pnpm-lock.yaml | 544 ++++++------------ .../services/team/TeamMcpConfigBuilder.ts | 34 +- ...enCodeRuntimePreflight.integration.test.ts | 4 +- .../team/TeamMcpConfigBuilder.test.ts | 12 +- 23 files changed, 336 insertions(+), 418 deletions(-) create mode 100644 .dockerignore create mode 100644 .node-version create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 landing/.npmrc diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c2d844c2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,52 @@ +# Dependencies installed inside the image +node_modules/ +landing/node_modules/ + +# Local build output +dist/ +dist-electron/ +dist-standalone/ +out/ +release/ +coverage/ +landing/.nuxt/ +landing/.output/ +electron.vite.config.*.mjs + +# Runtime and local caches +.git/ +.pnpm-store/ +.runtime-download/ +resources/runtime/* +!resources/runtime/.gitkeep +.eslintcache +.eslintcache-fast +*.tsbuildinfo + +# Local-only data +.claude/ +.home/ +.serena/ +.playwright-mcp/ +logs/ +*.log +.env +.env.* + +# OS and editor noise +.DS_Store +Thumbs.db +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Local scratch artifacts +notification_example/ +temp/ +eslint-fix/ +remotion/* +.tmp-* +agent-teams-reference-fix-*.png +ORCHESTRATOR_RELEASE_RUNBOOK.local.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6856f31..c53b510c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - name: Restore pnpm node-gyp executable bit @@ -102,7 +102,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - name: Restore pnpm node-gyp executable bit @@ -136,7 +136,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - name: Install dependencies diff --git a/.github/workflows/codex-runtime-smoke.yml b/.github/workflows/codex-runtime-smoke.yml index c162a884..cb902999 100644 --- a/.github/workflows/codex-runtime-smoke.yml +++ b/.github/workflows/codex-runtime-smoke.yml @@ -58,7 +58,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - name: Install dependencies diff --git a/.github/workflows/landing.yml b/.github/workflows/landing.yml index 62325b21..f61bf286 100644 --- a/.github/workflows/landing.yml +++ b/.github/workflows/landing.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: 22 + node-version-file: .node-version cache: npm cache-dependency-path: landing/package-lock.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d173a17d..cefefe63 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,7 +42,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - name: Restore pnpm node-gyp executable bit @@ -334,7 +334,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - name: Setup Python for node-gyp @@ -455,7 +455,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - name: Setup Python for node-gyp @@ -577,7 +577,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: 22 + node-version-file: .node-version cache: pnpm - name: Setup Python for node-gyp diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..b832e400 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24.16.0 diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..b832e400 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.16.0 diff --git a/agent-teams-controller/package.json b/agent-teams-controller/package.json index ce27fce1..902e6d05 100644 --- a/agent-teams-controller/package.json +++ b/agent-teams-controller/package.json @@ -14,6 +14,6 @@ "test:watch": "vitest --config vitest.config.js" }, "engines": { - "node": ">=20" + "node": ">=24.16.0 <25" } } diff --git a/docker/Dockerfile b/docker/Dockerfile index 51fe7153..2959e570 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -8,35 +8,55 @@ # Run: docker run -p 3456:3456 -v ~/.claude:/data/.claude:ro agent-teams-ai # ============================================================================= -FROM node:20-slim AS builder +ARG NODE_VERSION=24.16.0 + +FROM node:${NODE_VERSION}-slim AS base WORKDIR /app # Enable corepack for pnpm RUN corepack enable +FROM base AS builder + +# Native dependencies such as node-pty may need source builds on slim images. +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 make g++ \ + && rm -rf /var/lib/apt/lists/* + # Install dependencies first (better layer caching) COPY package.json pnpm-lock.yaml ./ +COPY patches ./patches RUN pnpm install --frozen-lockfile # Copy source and build COPY . . -RUN pnpm standalone:build +RUN AGENT_TEAMS_DISABLE_SOURCEMAPS=1 pnpm standalone:build # ============================================================================= -# Production stage — minimal image with only the built output +# Production dependencies stage # ============================================================================= -FROM node:20-slim +FROM base AS prod-deps -WORKDIR /app +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 make g++ \ + && rm -rf /var/lib/apt/lists/* -# Enable corepack for pnpm -RUN corepack enable - -# Copy package files and install production-only dependencies +# Install production-only dependencies # (fastify, @fastify/cors, @fastify/static are externalized from the bundle) -COPY --from=builder /app/package.json /app/pnpm-lock.yaml ./ -RUN pnpm install --frozen-lockfile --prod +COPY package.json pnpm-lock.yaml ./ +COPY patches ./patches +RUN pnpm install --frozen-lockfile --prod --ignore-scripts \ + && pnpm rebuild node-pty cpu-features ssh2 + +# ============================================================================= +# Production stage - minimal image with only runtime dependencies and built output +# ============================================================================= +FROM base + +COPY --from=prod-deps /app/package.json /app/pnpm-lock.yaml ./ +COPY --from=prod-deps /app/node_modules ./node_modules +COPY --from=builder /app/agent-teams-controller ./agent-teams-controller # Copy built standalone server and renderer output COPY --from=builder /app/dist-standalone ./dist-standalone diff --git a/docker/vite.standalone.config.ts b/docker/vite.standalone.config.ts index 8179e125..703cd07f 100644 --- a/docker/vite.standalone.config.ts +++ b/docker/vite.standalone.config.ts @@ -14,6 +14,7 @@ import type { Plugin } from 'vite' // `vite build --config docker/vite.standalone.config.ts`, so __dirname // is docker/. All paths must resolve relative to the repo root. const ROOT = resolve(__dirname, '..') +const sourceMapsEnabled = process.env.AGENT_TEAMS_DISABLE_SOURCEMAPS !== '1' // Node.js built-in modules that should be externalized const nodeBuiltins = new Set([ @@ -35,11 +36,13 @@ function nativeModuleStub(): Plugin { const STUB_ID = '\0native-stub' return { name: 'native-module-stub', + enforce: 'pre', resolveId(source) { if (source.endsWith('.node')) return STUB_ID return null }, load(id) { + if (id.endsWith('.node')) return 'export default {}' if (id === STUB_ID) return 'export default {}' return null } @@ -63,6 +66,8 @@ export const ipcMain = { handle: noop, on: noop, removeHandler: noop }; export const shell = { openPath: noop, openExternal: noop }; export const dialog = { showOpenDialog: async () => ({ canceled: true, filePaths: [] }) }; export const Notification = class { show() {} }; +export const nativeImage = { createFromPath: () => proxyObj, createEmpty: () => proxyObj }; +export const net = { fetch: globalThis.fetch }; export const safeStorage = { isEncryptionAvailable: () => false, encryptString: noop, decryptString: () => '' }; export const screen = proxyObj; export default proxyObj; @@ -87,6 +92,7 @@ export default defineConfig({ plugins: [nativeModuleStub(), electronStub()], resolve: { alias: { + '@features': resolve(ROOT, 'src/features'), '@main': resolve(ROOT, 'src/main'), '@shared': resolve(ROOT, 'src/shared'), '@preload': resolve(ROOT, 'src/preload') @@ -99,7 +105,7 @@ export default defineConfig({ }, build: { outDir: 'dist-standalone', - target: 'node20', + target: 'node24', ssr: true, rollupOptions: { input: { @@ -119,6 +125,6 @@ export default defineConfig({ } }, minify: false, - sourcemap: true + sourcemap: sourceMapsEnabled } }) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 1c6486c5..e157ceea 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -55,6 +55,8 @@ const sentrySourceMapTargets = { }, } as const +const sourceMapSetting = process.env.AGENT_TEAMS_DISABLE_SOURCEMAPS === '1' ? false : 'hidden' + // Sentry source map upload - only active in CI when SENTRY_AUTH_TOKEN is set. function createSentryPlugins(target: keyof typeof sentrySourceMapTargets): Plugin[] { if (!process.env.SENTRY_AUTH_TOKEN) return [] @@ -98,7 +100,7 @@ export default defineConfig({ commonjsOptions: { strictRequires: [/node_modules\/.*ssh2\//], }, - sourcemap: 'hidden', + sourcemap: sourceMapSetting, outDir: 'dist-electron/main', rollupOptions: { input: { @@ -169,7 +171,7 @@ export default defineConfig({ }, plugins: [react(), ...createSentryPlugins('renderer')], build: { - sourcemap: 'hidden', + sourcemap: sourceMapSetting, rollupOptions: { input: { index: resolve(__dirname, 'src/renderer/index.html') diff --git a/landing/.npmrc b/landing/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/landing/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/landing/package-lock.json b/landing/package-lock.json index 8a4d6beb..5841cc0b 100644 --- a/landing/package-lock.json +++ b/landing/package-lock.json @@ -35,6 +35,9 @@ "vitepress": "2.0.0-alpha.17", "vitepress-codeblock-collapse": "^1.0.0", "vitepress-plugin-llms": "^1.12.2" + }, + "engines": { + "node": ">=24.16.0 <25" } }, "node_modules/@alloc/quick-lru": { diff --git a/landing/package.json b/landing/package.json index 9d0ec642..fda278ca 100644 --- a/landing/package.json +++ b/landing/package.json @@ -2,6 +2,9 @@ "name": "agent-teams-landing", "private": true, "type": "module", + "engines": { + "node": ">=24.16.0 <25" + }, "scripts": { "dev": "nuxt dev", "build": "nuxt build", diff --git a/mcp-server/package.json b/mcp-server/package.json index 35ccc15e..71e2a285 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -41,13 +41,13 @@ "zod": "^4.3.6" }, "devDependencies": { + "@types/node": "^24.12.4", "tsup": "^8.5.1", "tsx": "^4.21.0", "typescript": "^5.8.2", - "vitest": "^3.1.4", - "@types/node": "^22.15.18" + "vitest": "^3.1.4" }, "engines": { - "node": ">=20" + "node": ">=24.16.0 <25" } } diff --git a/mcp-server/tsup.config.ts b/mcp-server/tsup.config.ts index 86520de5..01d2b622 100644 --- a/mcp-server/tsup.config.ts +++ b/mcp-server/tsup.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ entry: ['src/index.ts'], format: ['esm'], - target: 'node20', + target: 'node24', platform: 'node', outDir: 'dist', clean: true, diff --git a/package.json b/package.json index 48c25107..9b23c28f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "bugs": { "url": "https://github.com/777genius/agent-teams-ai/issues" }, + "engines": { + "node": ">=24.16.0 <25" + }, "main": "dist-electron/main/index.cjs", "scripts": { "dev": "node ./scripts/dev-with-runtime.mjs", @@ -83,7 +86,7 @@ "test:coverage": "vitest run --coverage", "test:coverage:critical": "vitest run --coverage --config vitest.critical.config.ts", "standalone": "tsx src/main/standalone.ts", - "standalone:build": "electron-vite build && vite build --config docker/vite.standalone.config.ts", + "standalone:build": "node --max-old-space-size=8192 ./node_modules/electron-vite/bin/electron-vite.js build && node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --config docker/vite.standalone.config.ts", "standalone:start": "node dist-standalone/index.cjs", "prepare": "husky", "postinstall": "electron-rebuild -f -o node-pty,ssh2,cpu-features || echo 'native Electron rebuild failed (terminal/ssh features may be degraded)'" @@ -212,7 +215,7 @@ "@tailwindcss/typography": "^0.5.19", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", - "@types/node": "^25.0.7", + "@types/node": "^24.12.4", "@types/pidusage": "2.0.5", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", @@ -388,7 +391,7 @@ } ] }, - "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319", + "packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800", "pnpm": { "overrides": { "@hono/node-server@1": "1.19.13", diff --git a/packages/agent-graph/package.json b/packages/agent-graph/package.json index fef2c312..8eca5d33 100644 --- a/packages/agent-graph/package.json +++ b/packages/agent-graph/package.json @@ -3,6 +3,9 @@ "version": "0.1.0", "private": true, "type": "module", + "engines": { + "node": ">=24.16.0 <25" + }, "main": "src/index.ts", "types": "src/index.ts", "exports": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5afc456..403e7eda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -401,7 +401,7 @@ importers: version: 4.0.4 '@eslint-community/eslint-plugin-eslint-comments': specifier: ^4.6.0 - version: 4.6.0(eslint@9.39.4(jiti@1.21.7)) + version: 4.6.0(eslint@9.39.4(jiti@2.7.0)) '@eslint/js': specifier: ^9.39.2 version: 9.39.2 @@ -418,8 +418,8 @@ importers: specifier: ^4.0.4 version: 4.0.4 '@types/node': - specifier: ^25.0.7 - version: 25.0.7 + specifier: ^24.12.4 + version: 24.12.4 '@types/pidusage': specifier: 2.0.5 version: 2.0.5 @@ -434,10 +434,10 @@ importers: version: 1.15.5 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.7.0(vite@6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.7.0(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/coverage-v8': specifier: ^3.1.4 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) autoprefixer: specifier: ^10.4.17 version: 10.4.23(postcss@8.5.10) @@ -449,43 +449,43 @@ importers: version: 26.8.1(electron-builder-squirrel-windows@26.8.1) electron-vite: specifier: ^5.0.0 - version: 5.0.0(@swc/core@1.15.33)(vite@6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 5.0.0(@swc/core@1.15.33)(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) eslint: specifier: ^9.39.4 - version: 9.39.4(jiti@1.21.7) + version: 9.39.4(jiti@2.7.0) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.4(jiti@1.21.7)) + version: 10.1.8(eslint@9.39.4(jiti@2.7.0)) eslint-import-resolver-typescript: specifier: ^4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) + version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-boundaries: specifier: ^5.3.1 - version: 5.3.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) + version: 5.3.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) + version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: specifier: ^6.10.2 - version: 6.10.2(eslint@9.39.4(jiti@1.21.7)) + version: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.39.4(jiti@1.21.7)) + version: 7.37.5(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-hooks: specifier: ^7.0.1 - version: 7.0.1(eslint@9.39.4(jiti@1.21.7)) + version: 7.0.1(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-refresh: specifier: ^0.4.26 - version: 0.4.26(eslint@9.39.4(jiti@1.21.7)) + version: 0.4.26(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-security: specifier: ^3.0.1 version: 3.0.1 eslint-plugin-simple-import-sort: specifier: ^12.1.1 - version: 12.1.1(eslint@9.39.4(jiti@1.21.7)) + version: 12.1.1(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-sonarjs: specifier: ^3.0.6 - version: 3.0.6(eslint@9.39.4(jiti@1.21.7)) + version: 3.0.6(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-tailwindcss: specifier: ^3.18.2 version: 3.18.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0)) @@ -500,10 +500,10 @@ importers: version: 9.1.7 i18next-cli: specifier: 1.58.0 - version: 1.58.0(@types/node@25.0.7)(i18next@26.2.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(typescript@5.9.3) + version: 1.58.0(@types/node@24.12.4)(i18next@26.2.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(typescript@5.9.3) knip: specifier: ^5.82.1 - version: 5.82.1(@types/node@25.0.7)(typescript@5.9.3) + version: 5.82.1(@types/node@24.12.4)(typescript@5.9.3) lint-staged: specifier: ^16.2.7 version: 16.2.7 @@ -527,13 +527,13 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.54.0 - version: 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + version: 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) vite: specifier: ^6.4.2 - version: 6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + version: 6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) vitest: specifier: ^3.1.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) agent-teams-controller: {} @@ -635,8 +635,8 @@ importers: version: 4.3.6 devDependencies: '@types/node': - specifier: ^22.15.18 - version: 22.19.15 + specifier: ^24.12.4 + version: 24.12.4 tsup: specifier: ^8.5.1 version: 8.5.1(@swc/core@1.15.33)(jiti@2.7.0)(postcss@8.5.10)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) @@ -648,7 +648,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.15)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) packages/agent-graph: dependencies: @@ -4722,11 +4722,8 @@ packages: '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@22.19.15': - resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} - - '@types/node@24.10.12': - resolution: {integrity: sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==} + '@types/node@24.12.4': + resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} '@types/node@25.0.7': resolution: {integrity: sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w==} @@ -10726,9 +10723,6 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -11858,10 +11852,10 @@ snapshots: '@borewit/text-codec@0.2.1': {} - '@boundaries/elements@1.1.2(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7))': + '@boundaries/elements@1.1.2(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0))': dependencies: eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) handlebars: 4.7.9 is-core-module: 2.16.1 micromatch: 4.0.8 @@ -12667,17 +12661,12 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.6.0(eslint@9.39.4(jiti@1.21.7))': + '@eslint-community/eslint-plugin-eslint-comments@4.6.0(eslint@9.39.4(jiti@2.7.0))': dependencies: escape-string-regexp: 4.0.0 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) ignore: 7.0.5 - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@1.21.7))': - dependencies: - eslint: 9.39.4(jiti@1.21.7) - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': dependencies: eslint: 9.39.4(jiti@2.7.0) @@ -12869,122 +12858,122 @@ snapshots: '@inquirer/ansi@2.0.5': {} - '@inquirer/checkbox@5.1.5(@types/node@25.0.7)': + '@inquirer/checkbox@5.1.5(@types/node@24.12.4)': dependencies: '@inquirer/ansi': 2.0.5 - '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/core': 11.1.10(@types/node@24.12.4) '@inquirer/figures': 2.0.5 - '@inquirer/type': 4.0.5(@types/node@25.0.7) + '@inquirer/type': 4.0.5(@types/node@24.12.4) optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 - '@inquirer/confirm@6.0.13(@types/node@25.0.7)': + '@inquirer/confirm@6.0.13(@types/node@24.12.4)': dependencies: - '@inquirer/core': 11.1.10(@types/node@25.0.7) - '@inquirer/type': 4.0.5(@types/node@25.0.7) + '@inquirer/core': 11.1.10(@types/node@24.12.4) + '@inquirer/type': 4.0.5(@types/node@24.12.4) optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 - '@inquirer/core@11.1.10(@types/node@25.0.7)': + '@inquirer/core@11.1.10(@types/node@24.12.4)': dependencies: '@inquirer/ansi': 2.0.5 '@inquirer/figures': 2.0.5 - '@inquirer/type': 4.0.5(@types/node@25.0.7) + '@inquirer/type': 4.0.5(@types/node@24.12.4) cli-width: 4.1.0 fast-wrap-ansi: 0.2.2 mute-stream: 3.0.0 signal-exit: 4.1.0 optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 - '@inquirer/editor@5.1.2(@types/node@25.0.7)': + '@inquirer/editor@5.1.2(@types/node@24.12.4)': dependencies: - '@inquirer/core': 11.1.10(@types/node@25.0.7) - '@inquirer/external-editor': 3.0.0(@types/node@25.0.7) - '@inquirer/type': 4.0.5(@types/node@25.0.7) + '@inquirer/core': 11.1.10(@types/node@24.12.4) + '@inquirer/external-editor': 3.0.0(@types/node@24.12.4) + '@inquirer/type': 4.0.5(@types/node@24.12.4) optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 - '@inquirer/expand@5.0.14(@types/node@25.0.7)': + '@inquirer/expand@5.0.14(@types/node@24.12.4)': dependencies: - '@inquirer/core': 11.1.10(@types/node@25.0.7) - '@inquirer/type': 4.0.5(@types/node@25.0.7) + '@inquirer/core': 11.1.10(@types/node@24.12.4) + '@inquirer/type': 4.0.5(@types/node@24.12.4) optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 - '@inquirer/external-editor@3.0.0(@types/node@25.0.7)': + '@inquirer/external-editor@3.0.0(@types/node@24.12.4)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 '@inquirer/figures@2.0.5': {} - '@inquirer/input@5.0.13(@types/node@25.0.7)': + '@inquirer/input@5.0.13(@types/node@24.12.4)': dependencies: - '@inquirer/core': 11.1.10(@types/node@25.0.7) - '@inquirer/type': 4.0.5(@types/node@25.0.7) + '@inquirer/core': 11.1.10(@types/node@24.12.4) + '@inquirer/type': 4.0.5(@types/node@24.12.4) optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 - '@inquirer/number@4.0.13(@types/node@25.0.7)': + '@inquirer/number@4.0.13(@types/node@24.12.4)': dependencies: - '@inquirer/core': 11.1.10(@types/node@25.0.7) - '@inquirer/type': 4.0.5(@types/node@25.0.7) + '@inquirer/core': 11.1.10(@types/node@24.12.4) + '@inquirer/type': 4.0.5(@types/node@24.12.4) optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 - '@inquirer/password@5.0.13(@types/node@25.0.7)': + '@inquirer/password@5.0.13(@types/node@24.12.4)': dependencies: '@inquirer/ansi': 2.0.5 - '@inquirer/core': 11.1.10(@types/node@25.0.7) - '@inquirer/type': 4.0.5(@types/node@25.0.7) + '@inquirer/core': 11.1.10(@types/node@24.12.4) + '@inquirer/type': 4.0.5(@types/node@24.12.4) optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 - '@inquirer/prompts@8.4.3(@types/node@25.0.7)': + '@inquirer/prompts@8.4.3(@types/node@24.12.4)': dependencies: - '@inquirer/checkbox': 5.1.5(@types/node@25.0.7) - '@inquirer/confirm': 6.0.13(@types/node@25.0.7) - '@inquirer/editor': 5.1.2(@types/node@25.0.7) - '@inquirer/expand': 5.0.14(@types/node@25.0.7) - '@inquirer/input': 5.0.13(@types/node@25.0.7) - '@inquirer/number': 4.0.13(@types/node@25.0.7) - '@inquirer/password': 5.0.13(@types/node@25.0.7) - '@inquirer/rawlist': 5.2.9(@types/node@25.0.7) - '@inquirer/search': 4.1.9(@types/node@25.0.7) - '@inquirer/select': 5.1.5(@types/node@25.0.7) + '@inquirer/checkbox': 5.1.5(@types/node@24.12.4) + '@inquirer/confirm': 6.0.13(@types/node@24.12.4) + '@inquirer/editor': 5.1.2(@types/node@24.12.4) + '@inquirer/expand': 5.0.14(@types/node@24.12.4) + '@inquirer/input': 5.0.13(@types/node@24.12.4) + '@inquirer/number': 4.0.13(@types/node@24.12.4) + '@inquirer/password': 5.0.13(@types/node@24.12.4) + '@inquirer/rawlist': 5.2.9(@types/node@24.12.4) + '@inquirer/search': 4.1.9(@types/node@24.12.4) + '@inquirer/select': 5.1.5(@types/node@24.12.4) optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 - '@inquirer/rawlist@5.2.9(@types/node@25.0.7)': + '@inquirer/rawlist@5.2.9(@types/node@24.12.4)': dependencies: - '@inquirer/core': 11.1.10(@types/node@25.0.7) - '@inquirer/type': 4.0.5(@types/node@25.0.7) + '@inquirer/core': 11.1.10(@types/node@24.12.4) + '@inquirer/type': 4.0.5(@types/node@24.12.4) optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 - '@inquirer/search@4.1.9(@types/node@25.0.7)': + '@inquirer/search@4.1.9(@types/node@24.12.4)': dependencies: - '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/core': 11.1.10(@types/node@24.12.4) '@inquirer/figures': 2.0.5 - '@inquirer/type': 4.0.5(@types/node@25.0.7) + '@inquirer/type': 4.0.5(@types/node@24.12.4) optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 - '@inquirer/select@5.1.5(@types/node@25.0.7)': + '@inquirer/select@5.1.5(@types/node@24.12.4)': dependencies: '@inquirer/ansi': 2.0.5 - '@inquirer/core': 11.1.10(@types/node@25.0.7) + '@inquirer/core': 11.1.10(@types/node@24.12.4) '@inquirer/figures': 2.0.5 - '@inquirer/type': 4.0.5(@types/node@25.0.7) + '@inquirer/type': 4.0.5(@types/node@24.12.4) optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 - '@inquirer/type@4.0.5(@types/node@25.0.7)': + '@inquirer/type@4.0.5(@types/node@24.12.4)': optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 '@intlify/bundle-utils@10.0.1(vue-i18n@10.0.8(vue@3.5.30(typescript@5.9.3)))': dependencies: @@ -15676,7 +15665,7 @@ snapshots: dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 25.0.7 + '@types/node': 24.12.4 '@types/responselike': 1.0.3 '@types/chai@5.2.3': @@ -15686,7 +15675,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 '@types/d3-array@3.2.2': {} @@ -15819,7 +15808,7 @@ snapshots: '@types/fs-extra@9.0.13': dependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 '@types/geojson@7946.0.16': {} @@ -15835,7 +15824,7 @@ snapshots: '@types/keyv@3.1.4': dependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 '@types/linkify-it@5.0.0': {} @@ -15854,23 +15843,20 @@ snapshots: '@types/mysql@2.15.27': dependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 '@types/node@18.19.130': dependencies: undici-types: 5.26.5 - '@types/node@22.19.15': - dependencies: - undici-types: 6.21.0 - - '@types/node@24.10.12': + '@types/node@24.12.4': dependencies: undici-types: 7.16.0 '@types/node@25.0.7': dependencies: undici-types: 7.16.0 + optional: true '@types/pg-pool@2.0.7': dependencies: @@ -15878,7 +15864,7 @@ snapshots: '@types/pg@8.15.6': dependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 pg-protocol: 1.13.0 pg-types: 2.2.0 @@ -15886,7 +15872,7 @@ snapshots: '@types/plist@3.0.5': dependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 xmlbuilder: 15.1.1 optional: true @@ -15902,7 +15888,7 @@ snapshots: '@types/responselike@1.0.3': dependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 '@types/ssh2@1.15.5': dependencies: @@ -15910,7 +15896,7 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 '@types/trusted-types@2.0.7': optional: true @@ -15932,22 +15918,22 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 '@types/yauzl@2.10.3': dependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 optional: true - '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.54.0 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -15971,14 +15957,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.54.0 debug: 4.4.3 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -16031,13 +16017,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -16089,29 +16075,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.54.0 '@typescript-eslint/types': 8.54.0 '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - eslint: 9.39.4(jiti@1.21.7) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - optional: true - '@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) @@ -16224,7 +16198,7 @@ snapshots: - rollup - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitejs/plugin-react@4.7.0(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.28.6 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) @@ -16232,7 +16206,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -16260,7 +16234,7 @@ snapshots: vite: 7.3.3(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) vue: 3.5.34(typescript@5.9.3) - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -16275,7 +16249,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -16287,21 +16261,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@22.19.15)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.2(@types/node@22.19.15)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) - - '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -17996,7 +17962,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-vite@5.0.0(@swc/core@1.15.33)(vite@6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): + electron-vite@5.0.0(@swc/core@1.15.33)(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) @@ -18004,7 +17970,7 @@ snapshots: esbuild: 0.25.12 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) optionalDependencies: '@swc/core': 1.15.33 transitivePeerDependencies: @@ -18025,7 +17991,7 @@ snapshots: electron@40.10.0: dependencies: '@electron/get': 2.0.3 - '@types/node': 24.10.12 + '@types/node': 24.12.4 extract-zip: 2.0.1 transitivePeerDependencies: - supports-color @@ -18317,9 +18283,9 @@ snapshots: '@eslint/compat': 2.0.3(eslint@9.39.4(jiti@2.7.0)) eslint: 9.39.4(jiti@2.7.0) - eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@1.21.7)): + eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)): dependencies: - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) eslint-flat-config-utils@3.0.2: dependencies: @@ -18341,10 +18307,10 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 4.4.3 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) get-tsconfig: 4.13.0 is-bun-module: 2.0.0 @@ -18352,8 +18318,8 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -18361,24 +18327,24 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.7.0) - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.4(jiti@1.21.7) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-boundaries@5.3.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-boundaries@5.3.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)): dependencies: - '@boundaries/elements': 1.1.2(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) + '@boundaries/elements': 1.1.2(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) chalk: 4.1.2 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) micromatch: 4.0.8 transitivePeerDependencies: - '@typescript-eslint/parser' @@ -18390,26 +18356,6 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.7.0) - eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@1.21.7)): - dependencies: - '@package-json/types': 0.0.12 - '@typescript-eslint/types': 8.57.1 - comment-parser: 1.4.5 - debug: 4.4.3 - eslint: 9.39.4(jiti@1.21.7) - eslint-import-context: 0.1.9(unrs-resolver@1.11.1) - is-glob: 4.0.3 - minimatch: 9.0.7 - semver: 7.7.4 - stable-hash-x: 0.2.0 - unrs-resolver: 1.11.1 - optionalDependencies: - '@typescript-eslint/utils': 8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - optional: true - eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@package-json/types': 0.0.12 @@ -18429,7 +18375,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -18438,9 +18384,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@1.21.7)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.39.4(jiti@2.7.0)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -18452,7 +18398,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -18478,7 +18424,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.7.0)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -18488,7 +18434,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -18497,22 +18443,22 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@2.7.0)): dependencies: '@babel/core': 7.28.6 '@babel/parser': 7.28.6 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.4.26(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-react-refresh@0.4.26(eslint@9.39.4(jiti@2.7.0)): dependencies: - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) - eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.7.0)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -18520,7 +18466,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.2 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -18549,16 +18495,16 @@ snapshots: dependencies: safe-regex: 2.1.1 - eslint-plugin-simple-import-sort@12.1.1(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-simple-import-sort@12.1.1(eslint@9.39.4(jiti@2.7.0)): dependencies: - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) - eslint-plugin-sonarjs@3.0.6(eslint@9.39.4(jiti@1.21.7)): + eslint-plugin-sonarjs@3.0.6(eslint@9.39.4(jiti@2.7.0)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) functional-red-black-tree: 1.0.1 jsx-ast-utils-x: 0.1.0 lodash.merge: 4.6.2 @@ -18629,47 +18575,6 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.4(jiti@1.21.7): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.5 - '@eslint/js': 9.39.4 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.14.0 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.4 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 1.21.7 - transitivePeerDependencies: - - supports-color - eslint@9.39.4(jiti@2.7.0): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) @@ -19367,7 +19272,7 @@ snapshots: happy-dom@20.9.0: dependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 @@ -19600,7 +19505,7 @@ snapshots: husky@9.1.7: {} - i18next-cli@1.58.0(@types/node@25.0.7)(i18next@26.2.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(typescript@5.9.3): + i18next-cli@1.58.0(@types/node@24.12.4)(i18next@26.2.0(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(typescript@5.9.3): dependencies: '@croct/json5-parser': 0.2.2 '@swc/core': 1.15.33 @@ -19609,7 +19514,7 @@ snapshots: execa: 9.6.1 glob: 13.0.6 i18next-resources-for-ts: 2.1.0 - inquirer: 13.4.3(@types/node@25.0.7) + inquirer: 13.4.3(@types/node@24.12.4) jiti: 2.7.0 jsonc-parser: 3.3.1 magic-string: 0.30.21 @@ -19700,17 +19605,17 @@ snapshots: inline-style-parser@0.2.7: {} - inquirer@13.4.3(@types/node@25.0.7): + inquirer@13.4.3(@types/node@24.12.4): dependencies: '@inquirer/ansi': 2.0.5 - '@inquirer/core': 11.1.10(@types/node@25.0.7) - '@inquirer/prompts': 8.4.3(@types/node@25.0.7) - '@inquirer/type': 4.0.5(@types/node@25.0.7) + '@inquirer/core': 11.1.10(@types/node@24.12.4) + '@inquirer/prompts': 8.4.3(@types/node@24.12.4) + '@inquirer/type': 4.0.5(@types/node@24.12.4) mute-stream: 3.0.0 run-async: 4.0.6 rxjs: 7.8.2 optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 internal-slot@1.1.0: dependencies: @@ -20085,10 +19990,10 @@ snapshots: klona@2.0.6: {} - knip@5.82.1(@types/node@25.0.7)(typescript@5.9.3): + knip@5.82.1(@types/node@24.12.4)(typescript@5.9.3): dependencies: '@nodelib/fs.walk': 1.2.8 - '@types/node': 25.0.7 + '@types/node': 24.12.4 fast-glob: 3.3.3 formatly: 0.3.0 jiti: 2.6.1 @@ -23361,13 +23266,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3): + typescript-eslint@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.4(jiti@1.21.7) + '@typescript-eslint/utils': 8.54.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -23405,8 +23310,6 @@ snapshots: undici-types@5.26.5: {} - undici-types@6.21.0: {} - undici-types@7.16.0: {} undici@6.25.0: {} @@ -23739,34 +23642,13 @@ snapshots: dependencies: vite: 7.3.2(@types/node@25.0.7)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) - vite-node@3.2.4(@types/node@22.19.15)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vite-node@3.2.4(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.2(@types/node@22.19.15)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite-node@3.2.4(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.3.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -23856,7 +23738,7 @@ snapshots: transitivePeerDependencies: - supports-color - vite@6.4.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -23865,24 +23747,7 @@ snapshots: rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.7 - fsevents: 2.3.3 - jiti: 1.21.7 - sass: 1.98.0 - terser: 5.46.0 - tsx: 4.21.0 - yaml: 2.9.0 - - vite@7.3.2(@types/node@22.19.15)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): - dependencies: - esbuild: 0.27.4 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.10 - rollup: 4.59.0 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 22.19.15 + '@types/node': 24.12.4 fsevents: 2.3.3 jiti: 2.7.0 sass: 1.98.0 @@ -23890,7 +23755,7 @@ snapshots: tsx: 4.21.0 yaml: 2.9.0 - vite@7.3.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vite@7.3.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.27.4 fdir: 6.5.0(picomatch@4.0.4) @@ -23899,9 +23764,9 @@ snapshots: rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.7 + '@types/node': 24.12.4 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.7.0 sass: 1.98.0 terser: 5.46.0 tsx: 4.21.0 @@ -24013,11 +23878,11 @@ snapshots: - universal-cookie - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.15)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.4)(happy-dom@20.9.0)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@22.19.15)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -24035,55 +23900,12 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.2(@types/node@22.19.15)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@22.19.15)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.2(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@24.12.4)(jiti@2.7.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 22.19.15 - happy-dom: 20.9.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.9.0)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): - dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.3.0 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 7.3.2(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@25.0.7)(jiti@1.21.7)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 25.0.7 + '@types/node': 24.12.4 happy-dom: 20.9.0 transitivePeerDependencies: - jiti diff --git a/src/main/services/team/TeamMcpConfigBuilder.ts b/src/main/services/team/TeamMcpConfigBuilder.ts index bb134149..2f18d73c 100644 --- a/src/main/services/team/TeamMcpConfigBuilder.ts +++ b/src/main/services/team/TeamMcpConfigBuilder.ts @@ -47,7 +47,10 @@ const MCP_CONFIG_PREFIX = 'agent-teams-mcp-'; const MCP_CONFIG_REMOVE_RETRY_DELAYS_MS = [25, 75, 150] as const; const NODE_RUNTIME_PROBE_TIMEOUT_MS = 5_000; const ELECTRON_NODE_RUNTIME_PROBE_TIMEOUT_MS = 5_000; -const MIN_MCP_NODE_MAJOR_VERSION = 20; +// The packaged Electron runtime can lag the source toolchain patch version, +// so MCP launch validation pins the Node 24 runtime line, not .node-version. +const MIN_MCP_NODE_MAJOR_VERSION = 24; +const MAX_MCP_NODE_MAJOR_VERSION = 25; const NODE_RUNTIME_PROBE_SCRIPT = 'process.stdout.write(JSON.stringify({execPath:process.execPath,version:process.versions.node}))'; /** @@ -335,9 +338,9 @@ function parseNodeRuntimeProbeMetadata(stdout: string, command: string): NodeRun function assertSupportedMcpNodeRuntime(command: string, metadata: NodeRuntimeProbeMetadata): void { const major = parseNodeMajorVersion(metadata.version); - if (major === null || major < MIN_MCP_NODE_MAJOR_VERSION) { + if (major === null || major < MIN_MCP_NODE_MAJOR_VERSION || major >= MAX_MCP_NODE_MAJOR_VERSION) { throw new Error( - `${command} resolved ${metadata.path} with Node.js ${metadata.version}; Agent Teams MCP requires Node.js ${MIN_MCP_NODE_MAJOR_VERSION}+` + `${command} resolved ${metadata.path} with Node.js ${metadata.version}; Agent Teams MCP requires Node.js 24.x` ); } } @@ -392,21 +395,16 @@ async function probePackagedElectronNodeRuntime( emitProgress(options, 'electron-node-runtime', 'Checking bundled Electron Node runtime...'); try { - const { stdout } = await execCli( - process.execPath.trim(), - ['-e', 'process.stdout.write("agent-teams-electron-node-ok")'], - { - encoding: 'utf-8', - timeout: ELECTRON_NODE_RUNTIME_PROBE_TIMEOUT_MS, - env: { - ...process.env, - ...getPackagedElectronNodeEnv(), - }, - } - ); - if (stdout.trim() !== 'agent-teams-electron-node-ok') { - throw new Error('Electron Node runtime probe did not return the expected marker'); - } + const { stdout } = await execCli(process.execPath.trim(), ['-e', NODE_RUNTIME_PROBE_SCRIPT], { + encoding: 'utf-8', + timeout: ELECTRON_NODE_RUNTIME_PROBE_TIMEOUT_MS, + env: { + ...process.env, + ...getPackagedElectronNodeEnv(), + }, + }); + const metadata = parseNodeRuntimeProbeMetadata(stdout, process.execPath.trim()); + assertSupportedMcpNodeRuntime(process.execPath.trim(), metadata); _packagedElectronNodeRuntimeProbe = { ok: true }; } catch (error) { _packagedElectronNodeRuntimeProbe = { ok: false, error }; diff --git a/test/main/services/runtime/OpenCodeRuntimePreflight.integration.test.ts b/test/main/services/runtime/OpenCodeRuntimePreflight.integration.test.ts index 6de2e5e7..3228d3e7 100644 --- a/test/main/services/runtime/OpenCodeRuntimePreflight.integration.test.ts +++ b/test/main/services/runtime/OpenCodeRuntimePreflight.integration.test.ts @@ -55,6 +55,7 @@ vi.mock('@features/codex-runtime-installer/main', () => ({ import { resolveVerifiedOpenCodeRuntimeBinaryPath } from '../../../../src/main/services/infrastructure/OpenCodeRuntimeInstallerService'; import { ensureOpenCodeBridgeRuntimeBinaryEnv } from '../../../../src/main/services/runtime/openCodeBridgeRuntimeEnv'; import { buildProviderAwareCliEnv } from '../../../../src/main/services/runtime/providerAwareCliEnv'; +import { clearResolvedNodePathForTests } from '../../../../src/main/services/team/TeamMcpConfigBuilder'; import { execCli } from '../../../../src/main/utils/childProcess'; import { setAppDataBasePath } from '../../../../src/main/utils/pathDecoder'; import { clearShellEnvCache } from '../../../../src/main/utils/shellEnv'; @@ -72,6 +73,7 @@ describePosix('OpenCode packaged-runtime preflight integration', () => { tempDir = await mkdtemp(path.join(os.tmpdir(), 'opencode-prod-preflight-')); setAppDataBasePath(path.join(tempDir, 'app-data')); clearShellEnvCache(); + clearResolvedNodePathForTests(); originalPath = process.env.PATH; originalShell = process.env.SHELL; @@ -142,7 +144,7 @@ describePosix('OpenCode packaged-runtime preflight integration', () => { [ '#!/bin/sh', 'if [ "$1" = "-e" ]; then', - ' printf "{\\"execPath\\":\\"%s\\",\\"version\\":\\"%s\\"}" "$FAKE_NODE_PATH" "22.0.0"', + ' printf "{\\"execPath\\":\\"%s\\",\\"version\\":\\"%s\\"}" "$FAKE_NODE_PATH" "24.16.0"', ' exit 0', 'fi', 'echo "unexpected node args: $*" >&2', diff --git a/test/main/services/team/TeamMcpConfigBuilder.test.ts b/test/main/services/team/TeamMcpConfigBuilder.test.ts index 95825470..de3dcbd8 100644 --- a/test/main/services/team/TeamMcpConfigBuilder.test.ts +++ b/test/main/services/team/TeamMcpConfigBuilder.test.ts @@ -22,7 +22,7 @@ const hoisted = vi.hoisted(() => ({ version: '9.9.9-test', }, execCliMock: vi.fn(async () => ({ - stdout: JSON.stringify({ execPath: '/mock/node', version: '20.11.0' }), + stdout: JSON.stringify({ execPath: '/mock/node', version: '24.16.0' }), stderr: '', })), cachedShellEnv: null as NodeJS.ProcessEnv | null, @@ -68,7 +68,7 @@ import { } from '@main/services/team/TeamMcpConfigBuilder'; import { setAppDataBasePath, setClaudeBasePathOverride } from '@main/utils/pathDecoder'; -function nodeRuntimeProbeStdout(execPath: string, version = '20.11.0'): string { +function nodeRuntimeProbeStdout(execPath: string, version = '24.16.0'): string { return JSON.stringify({ execPath, version }); } @@ -385,7 +385,7 @@ describe('TeamMcpConfigBuilder', () => { createPackagedServerBundle(resourcesDir, '// packaged server'); setResourcesPath(resourcesDir); hoisted.execCliMock.mockResolvedValue({ - stdout: 'agent-teams-electron-node-ok', + stdout: nodeRuntimeProbeStdout(electronBinary, '24.15.0'), stderr: '', }); @@ -418,7 +418,7 @@ describe('TeamMcpConfigBuilder', () => { expect(hoisted.execCliMock).toHaveBeenCalledTimes(1); expect(hoisted.execCliMock).toHaveBeenCalledWith( electronBinary, - ['-e', 'process.stdout.write("agent-teams-electron-node-ok")'], + ['-e', expect.stringContaining('process.versions.node')], expect.objectContaining({ env: expect.objectContaining({ ELECTRON_RUN_AS_NODE: '1' }), }) @@ -528,11 +528,11 @@ describe('TeamMcpConfigBuilder', () => { if (env?.PATH?.split(path.delimiter)[0] === '/strict-shell-node-bin') { expect(command).toBe('node'); return { - stdout: nodeRuntimeProbeStdout('/strict-shell-node-bin/node', '20.11.0'), + stdout: nodeRuntimeProbeStdout('/strict-shell-node-bin/node', '24.16.0'), stderr: '', }; } - return { stdout: nodeRuntimeProbeStdout('/usr/bin/node', '18.19.0'), stderr: '' }; + return { stdout: nodeRuntimeProbeStdout('/usr/bin/node', '22.21.1'), stderr: '' }; }); try { From 636beb5e4220bc9f27e2a98c4410d1c3cb8d5af2 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 19:46:13 +0300 Subject: [PATCH 21/59] fix(scripts): quote Windows shell invocations --- scripts/dev-web.mjs | 16 +---- scripts/dev-with-runtime.mjs | 24 ++------ scripts/lib/windows-shell-spawn.mjs | 59 +++++++++++++++++++ scripts/prove-agent-cli-launch.mjs | 5 +- scripts/prove-opencode-mixed-recovery.mjs | 5 +- scripts/prove-opencode-semantic-gauntlet.mjs | 5 +- scripts/prove-opencode-semantic-messaging.mjs | 5 +- .../prove-opencode-semantic-model-matrix.mjs | 5 +- scripts/prove-opencode-team-provisioning.mjs | 5 +- scripts/prove-provider-launch-stress.mjs | 4 +- .../TeamProvisioningServicePrepare.test.ts | 23 ++++++-- 11 files changed, 99 insertions(+), 57 deletions(-) create mode 100644 scripts/lib/windows-shell-spawn.mjs diff --git a/scripts/dev-web.mjs b/scripts/dev-web.mjs index 159415ef..f3f56971 100644 --- a/scripts/dev-web.mjs +++ b/scripts/dev-web.mjs @@ -2,9 +2,10 @@ import path from 'node:path'; import process from 'node:process'; -import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +import { spawnWithWindowsShell } from './lib/windows-shell-spawn.mjs'; + const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); const standalonePort = process.env.STANDALONE_PORT?.trim() || '3456'; @@ -13,22 +14,11 @@ const corsOrigin = process.env.CORS_ORIGIN?.trim() || `http://127.0.0.1:${webPort},http://localhost:${webPort}`; -const WINDOWS_SHELL_COMMANDS = new Set(['pnpm', 'npm', 'npx', 'yarn', 'yarnpkg', 'corepack']); - -function shouldUseWindowsShell(cmd) { - if (process.platform !== 'win32') { - return false; - } - - return WINDOWS_SHELL_COMMANDS.has(path.basename(cmd).toLowerCase()); -} - function spawnProcess(cmd, args, env) { - return spawn(cmd, args, { + return spawnWithWindowsShell(cmd, args, { cwd: repoRoot, env: { ...process.env, ...env }, stdio: 'inherit', - shell: shouldUseWindowsShell(cmd), }); } diff --git a/scripts/dev-with-runtime.mjs b/scripts/dev-with-runtime.mjs index 1ac19992..c20f751c 100644 --- a/scripts/dev-with-runtime.mjs +++ b/scripts/dev-with-runtime.mjs @@ -4,11 +4,12 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; -import { spawnSync } from 'node:child_process'; import { once } from 'node:events'; import readline from 'node:readline'; import { fileURLToPath } from 'node:url'; +import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs'; + const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const uiRepoRoot = path.resolve(scriptDir, '..'); const runtimeRepoRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim() ?? ''; @@ -22,26 +23,10 @@ const scriptArgs = process.argv.slice(2); const shouldPrintRuntimePath = scriptArgs.includes('--print-runtime-path'); const electronViteArgs = scriptArgs.filter((arg) => arg !== '--print-runtime-path' && arg !== '--'); const runtimeDisplayName = 'teams orchestrator'; -const WINDOWS_SHELL_COMMANDS = new Set(['pnpm', 'npm', 'npx', 'yarn', 'yarnpkg', 'corepack']); - -function shouldUseWindowsShell(cmd) { - if (process.platform !== 'win32') { - return false; - } - - const extension = path.extname(cmd).toLowerCase(); - if (extension === '.cmd' || extension === '.bat') { - return true; - } - - const commandName = path.basename(cmd).toLowerCase(); - return WINDOWS_SHELL_COMMANDS.has(commandName); -} function runOrExit(cmd, args, options = {}) { - const result = spawnSync(cmd, args, { + const result = spawnSyncWithWindowsShell(cmd, args, { stdio: 'inherit', - shell: shouldUseWindowsShell(cmd), ...options, }); @@ -56,9 +41,8 @@ function runOrExit(cmd, args, options = {}) { } function runAndCapture(cmd, args, options = {}) { - const result = spawnSync(cmd, args, { + const result = spawnSyncWithWindowsShell(cmd, args, { encoding: 'utf8', - shell: shouldUseWindowsShell(cmd), ...options, }); diff --git a/scripts/lib/windows-shell-spawn.mjs b/scripts/lib/windows-shell-spawn.mjs new file mode 100644 index 00000000..002e6fef --- /dev/null +++ b/scripts/lib/windows-shell-spawn.mjs @@ -0,0 +1,59 @@ +import { spawn, spawnSync } from 'node:child_process'; +import path from 'node:path'; +import process from 'node:process'; + +const WINDOWS_SHELL_COMMANDS = new Set(['pnpm', 'npm', 'npx', 'yarn', 'yarnpkg', 'corepack']); + +export function quoteWindowsCmdArg(value) { + const text = String(value); + if (text.length === 0) { + return '""'; + } + if (!/[ \t\r\n"&|<>^()%!]/.test(text)) { + return text; + } + return `"${text.replace(/%/g, '%%').replace(/(["^&|<>])/g, '^$1')}"`; +} + +export function shouldUseWindowsShell(command) { + if (process.platform !== 'win32') { + return false; + } + + const extension = path.extname(command).toLowerCase(); + if (extension === '.cmd' || extension === '.bat') { + return true; + } + + return WINDOWS_SHELL_COMMANDS.has(path.basename(command).toLowerCase()); +} + +function toWindowsShellCommand(command, args) { + return [command, ...args].map(quoteWindowsCmdArg).join(' '); +} + +export function spawnWithWindowsShell(command, args, options = {}) { + if (!shouldUseWindowsShell(command)) { + return spawn(command, args, options); + } + + const safeOptions = { ...options }; + delete safeOptions.shell; + return spawn(toWindowsShellCommand(command, args), { + ...safeOptions, + shell: true, + }); +} + +export function spawnSyncWithWindowsShell(command, args, options = {}) { + if (!shouldUseWindowsShell(command)) { + return spawnSync(command, args, options); + } + + const safeOptions = { ...options }; + delete safeOptions.shell; + return spawnSync(toWindowsShellCommand(command, args), { + ...safeOptions, + shell: true, + }); +} diff --git a/scripts/prove-agent-cli-launch.mjs b/scripts/prove-agent-cli-launch.mjs index eaf63af5..980f0169 100644 --- a/scripts/prove-agent-cli-launch.mjs +++ b/scripts/prove-agent-cli-launch.mjs @@ -1,11 +1,11 @@ #!/usr/bin/env node -import { spawnSync } from 'node:child_process'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs'; +import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs'; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); @@ -26,7 +26,7 @@ if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) { console.log('Running agent CLI launch live smoke'); console.log(`Claude runtime: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`); -const result = spawnSync( +const result = spawnSyncWithWindowsShell( 'pnpm', [ 'exec', @@ -42,7 +42,6 @@ const result = spawnSync( cwd: repoRoot, env, stdio: 'inherit', - shell: process.platform === 'win32', } ); diff --git a/scripts/prove-opencode-mixed-recovery.mjs b/scripts/prove-opencode-mixed-recovery.mjs index 3a896a7f..59845aa2 100644 --- a/scripts/prove-opencode-mixed-recovery.mjs +++ b/scripts/prove-opencode-mixed-recovery.mjs @@ -1,6 +1,5 @@ #!/usr/bin/env node -import { spawnSync } from 'node:child_process'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; @@ -10,6 +9,7 @@ import { exitForSkippedPreflight, preflightOpenCodeLiveEnvironment, } from './lib/opencode-live-preflight.mjs'; +import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs'; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); @@ -40,7 +40,7 @@ console.log(`Multi-lane: ${env.OPENCODE_E2E_MIXED_RECOVERY_MULTI === '1' ? 'enab const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot }); exitForSkippedPreflight(preflight); -const result = spawnSync( +const result = spawnSyncWithWindowsShell( 'pnpm', [ 'exec', @@ -56,7 +56,6 @@ const result = spawnSync( cwd: repoRoot, env, stdio: 'inherit', - shell: process.platform === 'win32', } ); diff --git a/scripts/prove-opencode-semantic-gauntlet.mjs b/scripts/prove-opencode-semantic-gauntlet.mjs index 031b18a1..8366d98b 100644 --- a/scripts/prove-opencode-semantic-gauntlet.mjs +++ b/scripts/prove-opencode-semantic-gauntlet.mjs @@ -1,6 +1,5 @@ #!/usr/bin/env node -import { spawnSync } from 'node:child_process'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; @@ -10,6 +9,7 @@ import { exitForSkippedPreflight, preflightOpenCodeLiveEnvironment, } from './lib/opencode-live-preflight.mjs'; +import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs'; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); @@ -46,7 +46,7 @@ console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`) const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot }); exitForSkippedPreflight(preflight); -const result = spawnSync( +const result = spawnSyncWithWindowsShell( 'pnpm', [ 'exec', @@ -62,7 +62,6 @@ const result = spawnSync( cwd: repoRoot, env, stdio: 'inherit', - shell: process.platform === 'win32', } ); diff --git a/scripts/prove-opencode-semantic-messaging.mjs b/scripts/prove-opencode-semantic-messaging.mjs index 1eee1cb0..a62877a8 100644 --- a/scripts/prove-opencode-semantic-messaging.mjs +++ b/scripts/prove-opencode-semantic-messaging.mjs @@ -1,6 +1,5 @@ #!/usr/bin/env node -import { spawnSync } from 'node:child_process'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; @@ -10,6 +9,7 @@ import { exitForSkippedPreflight, preflightOpenCodeLiveEnvironment, } from './lib/opencode-live-preflight.mjs'; +import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs'; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); @@ -38,7 +38,7 @@ console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`) const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot }); exitForSkippedPreflight(preflight); -const result = spawnSync( +const result = spawnSyncWithWindowsShell( 'pnpm', [ 'exec', @@ -54,7 +54,6 @@ const result = spawnSync( cwd: repoRoot, env, stdio: 'inherit', - shell: process.platform === 'win32', } ); diff --git a/scripts/prove-opencode-semantic-model-matrix.mjs b/scripts/prove-opencode-semantic-model-matrix.mjs index 9f91862f..1b74e5c6 100644 --- a/scripts/prove-opencode-semantic-model-matrix.mjs +++ b/scripts/prove-opencode-semantic-model-matrix.mjs @@ -1,6 +1,5 @@ #!/usr/bin/env node -import { spawnSync } from 'node:child_process'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; @@ -10,6 +9,7 @@ import { exitForSkippedPreflight, preflightOpenCodeLiveEnvironment, } from './lib/opencode-live-preflight.mjs'; +import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs'; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); @@ -36,7 +36,7 @@ console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`) const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot }); exitForSkippedPreflight(preflight); -const result = spawnSync( +const result = spawnSyncWithWindowsShell( 'pnpm', [ 'exec', @@ -52,7 +52,6 @@ const result = spawnSync( cwd: repoRoot, env, stdio: 'inherit', - shell: process.platform === 'win32', } ); diff --git a/scripts/prove-opencode-team-provisioning.mjs b/scripts/prove-opencode-team-provisioning.mjs index 12673fd3..5cca86a3 100644 --- a/scripts/prove-opencode-team-provisioning.mjs +++ b/scripts/prove-opencode-team-provisioning.mjs @@ -1,6 +1,5 @@ #!/usr/bin/env node -import { spawnSync } from 'node:child_process'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; @@ -10,6 +9,7 @@ import { exitForSkippedPreflight, preflightOpenCodeLiveEnvironment, } from './lib/opencode-live-preflight.mjs'; +import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs'; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); @@ -38,7 +38,7 @@ console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`) const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot }); exitForSkippedPreflight(preflight); -const result = spawnSync( +const result = spawnSyncWithWindowsShell( 'pnpm', [ 'exec', @@ -54,7 +54,6 @@ const result = spawnSync( cwd: repoRoot, env, stdio: 'inherit', - shell: process.platform === 'win32', } ); diff --git a/scripts/prove-provider-launch-stress.mjs b/scripts/prove-provider-launch-stress.mjs index eaea932b..79665188 100644 --- a/scripts/prove-provider-launch-stress.mjs +++ b/scripts/prove-provider-launch-stress.mjs @@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url'; import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs'; import { preflightOpenCodeLiveEnvironment } from './lib/opencode-live-preflight.mjs'; +import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs'; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, '..'); @@ -69,7 +70,7 @@ if (preflight.skipped.length > 0 && process.env.PROVIDER_LAUNCH_STRESS_STRICT == env.PROVIDER_LAUNCH_STRESS_ORDER = preflight.order.join(','); console.log(`Runnable order: ${env.PROVIDER_LAUNCH_STRESS_ORDER}`); -const result = spawnSync( +const result = spawnSyncWithWindowsShell( 'pnpm', [ 'exec', @@ -85,7 +86,6 @@ const result = spawnSync( cwd: repoRoot, env, stdio: 'inherit', - shell: process.platform === 'win32', } ); diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts index 3b19dedc..2cfdbd9d 100644 --- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts @@ -312,10 +312,25 @@ function spawnRealCli( ) { const spawnOptions = options ?? {}; const needsWindowsCommandShell = process.platform === 'win32' && /\.(bat|cmd)$/i.test(command); - return spawn(command, [...args], { - ...spawnOptions, - ...(needsWindowsCommandShell ? { shell: true } : {}), - }); + if (needsWindowsCommandShell) { + const commandLine = [command, ...args].map(quoteWindowsCmdArg).join(' '); + return spawn(commandLine, { + ...spawnOptions, + shell: true, + }); + } + + return spawn(command, [...args], spawnOptions); +} + +function quoteWindowsCmdArg(value: string) { + if (value.length === 0) { + return '""'; + } + if (!/[ \t\r\n"&|<>^()%!]/.test(value)) { + return value; + } + return `"${value.replace(/%/g, '%%').replace(/(["^&|<>])/g, '^$1')}"`; } async function removeTempRoot(dirPath: string): Promise { From f237318c2921dc7f36348aaa24162cfa149e0038 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 19:46:24 +0300 Subject: [PATCH 22/59] fix(agent-teams): surface OpenCode runtime permissions --- .../services/team/TeamProvisioningService.ts | 730 ++++++++++++++- .../RuntimeToolApprovalCoordinator.ts | 4 + .../bridge/OpenCodeBridgeCommandContract.ts | 3 + .../bridge/OpenCodeReadinessBridge.ts | 8 +- .../TeamProvisioningLaunchFailurePolicy.ts | 13 +- .../runtime/OpenCodeTeamRuntimeAdapter.ts | 31 + .../team/runtime/TeamRuntimeAdapter.ts | 16 + src/main/services/team/runtime/index.ts | 2 + .../team/OpenCodeReadinessBridge.test.ts | 40 + .../team/OpenCodeTeamRuntimeAdapter.test.ts | 85 ++ .../RuntimeToolApprovalCoordinator.test.ts | 42 + .../TeamAgentLaunchMatrix.safe-e2e.test.ts | 880 ++++++++++++++++++ ...eamProvisioningLaunchFailurePolicy.test.ts | 5 + .../team/TeamProvisioningService.test.ts | 84 ++ 14 files changed, 1933 insertions(+), 10 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 86390c9b..4fe7464e 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -229,6 +229,7 @@ import { import { deriveMemberLaunchState, isAutoClearableLaunchFailureReason, + isCliProvisionedButNotAliveFailureReason, isNeverSpawnedDuringLaunchReason, } from './provisioning/TeamProvisioningLaunchFailurePolicy'; import { @@ -523,6 +524,8 @@ import type { TeamRuntimeLaunchResult, TeamRuntimeMemberLaunchEvidence, TeamRuntimeMemberSpec, + TeamRuntimePendingPermission, + TeamRuntimePermissionListResult, TeamRuntimePrepareResult, TeamRuntimeStopInput, } from './runtime'; @@ -540,6 +543,16 @@ type OpenCodeRuntimeMessageAdapter = TeamLaunchRuntimeAdapter & { ): Promise; }; +type OpenCodeRuntimePermissionListingAdapter = TeamLaunchRuntimeAdapter & { + listRuntimePermissions(input: { + teamName: string; + laneId: string; + cwd: string; + memberName?: string; + sessionId?: string | null; + }): Promise; +}; + /** * Kill a team CLI process using SIGKILL (uncatchable). * @@ -1077,6 +1090,8 @@ const OPENCODE_RUNTIME_BINARY_UNREACHABLE_DIAGNOSTIC = 'OpenCode runtime binary is not installed or not reachable by launch preflight.'; const OPENCODE_APP_MCP_UNREACHABLE_DIAGNOSTIC = 'OpenCode app MCP is unreachable. Retry launch to refresh the app MCP bridge.'; +const OPENCODE_PENDING_PERMISSION_REQUEST_PATTERN = + /\b(?:pending permission request(?:\(s\)|s)?|permission[_ -]blocked)\b/i; function pushUniqueLine(lines: string[], line: string): void { const trimmed = line.trim(); @@ -5031,6 +5046,14 @@ export class TeamProvisioningService { return adapter as OpenCodeRuntimeMessageAdapter; } + private getOpenCodeRuntimePermissionListingAdapter(): OpenCodeRuntimePermissionListingAdapter | null { + const adapter = this.getOpenCodeRuntimeAdapter(); + if (!adapter || typeof adapter.listRuntimePermissions !== 'function') { + return null; + } + return adapter as OpenCodeRuntimePermissionListingAdapter; + } + private resolveRuntimeRecipientProviderIdFromSources( memberName: string, config: TeamConfig | null | undefined, @@ -6261,6 +6284,19 @@ export class TeamProvisioningService { const reason = `opencode_direct_user_delivery_inline_observe_failed: ${getErrorMessage( error )}`; + await this.maybeSyncOpenCodeRuntimePermissionsAfterDelivery({ + teamName: input.teamName, + runId: input.runtimeRunId, + laneId: input.laneId, + memberName: input.memberName, + cwd: input.cwd, + sessionId: ledgerRecord.runtimeSessionId, + reason, + diagnostics: [ + `opencode_direct_user_delivery_inline_observe_attempt_${inlineObserveAttempt}`, + reason, + ], + }); ledgerRecord = await input.ledger.applyObservation({ id: ledgerRecord.id, responseObservation: { @@ -6294,6 +6330,17 @@ export class TeamProvisioningService { const observedResponse = this.normalizeOpenCodeDeliveryResponseObservation( observed.responseObservation ); + await this.maybeSyncOpenCodeRuntimePermissionsAfterDelivery({ + teamName: input.teamName, + runId: input.runtimeRunId, + laneId: input.laneId, + memberName: input.memberName, + cwd: input.cwd, + sessionId: observed.sessionId, + responseState: observedResponse?.state, + reason: observedResponse?.reason ?? observed.diagnostics[0], + diagnostics: observed.diagnostics, + }); const hadMessageSendToolError = this.hasOpenCodeObservedMessageSendToolCall(ledgerRecord); ledgerRecord = await input.ledger.applyObservation({ id: ledgerRecord.id, @@ -6773,15 +6820,16 @@ export class TeamProvisioningService { try { const changed = await this.enqueueLaunchStateStoreOperation(input.teamName, async () => { const previous = await this.launchStateStore.read(input.teamName).catch(() => null); - const directMember = previous?.members[input.memberName]; - const laneMemberEntry = Object.entries(previous?.members ?? {}).find( - ([, member]) => member.laneId === input.laneId - ); - const previousMember = directMember ?? laneMemberEntry?.[1]; - const previousMemberKey = directMember ? input.memberName : laneMemberEntry?.[0]; - if (!previous || !previousMember) { + const previousEntry = this.findPersistedLaunchMemberForLane({ + previousLaunchState: previous, + laneId: input.laneId, + memberName: input.memberName, + runId: input.runId, + }); + if (!previous || !previousEntry) { return false; } + const previousMember = previousEntry.member; if (!isPersistedOpenCodeSecondaryLaneMember(previousMember)) { return false; } @@ -6831,7 +6879,7 @@ export class TeamProvisioningService { launchPhase: previous.launchPhase, members: { ...previous.members, - [previousMemberKey ?? previousMember.name]: nextMember, + [previousEntry.key]: nextMember, }, updatedAt: observedAt, }); @@ -6854,6 +6902,617 @@ export class TeamProvisioningService { } } + private hasOpenCodePendingPermissionSignal(input: { + responseState?: OpenCodeMemberInboxDelivery['responseState']; + reason?: string | null; + diagnostics?: readonly string[]; + }): boolean { + if (input.responseState === 'permission_blocked') { + return true; + } + const text = [input.reason ?? undefined, ...(input.diagnostics ?? [])] + .filter((value): value is string => Boolean(value?.trim())) + .join('\n'); + return OPENCODE_PENDING_PERMISSION_REQUEST_PATTERN.test(text); + } + + private findPersistedLaunchMemberForLane(input: { + previousLaunchState: PersistedTeamLaunchSnapshot | null | undefined; + laneId: string; + memberName: string; + runId?: string | null; + }): { key: string; member: PersistedTeamLaunchMemberState } | null { + const members = input.previousLaunchState?.members; + if (!members) { + return null; + } + const laneId = input.laneId.trim() || 'primary'; + const memberName = input.memberName.trim(); + const runId = input.runId?.trim(); + const candidates = Object.entries(members).filter(([key, member]) => { + const storedName = this.resolvePersistedLaunchMemberDisplayName(key, member); + if (storedName !== memberName) { + return false; + } + if ((member.laneId?.trim() || 'primary') !== laneId) { + return false; + } + const memberRunId = member.runtimeRunId?.trim(); + return !(runId && memberRunId && memberRunId !== runId); + }); + if (candidates.length === 0) { + return null; + } + const direct = candidates.find(([key]) => key === memberName); + const [key, member] = direct ?? candidates[0]!; + return { key, member }; + } + + private resolvePersistedLaunchMemberDisplayName( + key: string, + member: PersistedTeamLaunchMemberState + ): string { + const storedName = member.name?.trim(); + const laneId = member.laneId?.trim(); + const laneMemberName = + (laneId ? this.extractOpenCodeRuntimeLaneMemberName(laneId) : null) ?? + this.extractOpenCodeRuntimeLaneMemberName(key); + if (storedName && storedName !== laneId && storedName !== key.trim()) { + return storedName; + } + return laneMemberName ?? storedName ?? key.trim(); + } + + private async maybeSyncOpenCodeRuntimePermissionsAfterDelivery(input: { + teamName: string; + runId?: string | null; + laneId: string; + memberName: string; + cwd: string; + sessionId?: string | null; + responseState?: OpenCodeMemberInboxDelivery['responseState']; + reason?: string | null; + diagnostics?: readonly string[]; + teamColor?: string; + teamDisplayName?: string; + }): Promise { + if (!input.runId?.trim()) { + return; + } + const runId = input.runId.trim(); + if (this.getTrackedRunId(input.teamName) !== runId) { + return; + } + if (!this.hasOpenCodePendingPermissionSignal(input)) { + return; + } + + const adapter = this.getOpenCodeRuntimePermissionListingAdapter(); + if (!adapter) { + logger.warn( + `[${input.teamName}] OpenCode runtime permission signal observed for ${input.memberName}, but permission listing bridge is unavailable.` + ); + return; + } + + let listed: { permissions: TeamRuntimePendingPermission[]; diagnostics: string[] }; + try { + listed = await adapter.listRuntimePermissions({ + teamName: input.teamName, + laneId: input.laneId, + cwd: input.cwd, + memberName: input.memberName, + sessionId: input.sessionId, + }); + } catch (error) { + logger.warn( + `[${input.teamName}] Failed to list OpenCode runtime permissions for ${input.memberName}: ${getErrorMessage(error)}` + ); + return; + } + + if (this.getTrackedRunId(input.teamName) !== runId) { + return; + } + + const pendingPermissions = listed.permissions.filter((permission) => + this.isOpenCodeRuntimePermissionForDeliveryTarget(input, permission) + ); + if (pendingPermissions.length === 0) { + const listedDiagnostics = listed.diagnostics.length + ? ` Diagnostics: ${listed.diagnostics.join(' | ')}` + : ''; + logger.warn( + `[${input.teamName}] OpenCode runtime permission signal observed for ${input.memberName}, but bridge listed no matching pending permissions.${listedDiagnostics}` + ); + return; + } + + const previousLaunchState = await this.launchStateStore.read(input.teamName).catch(() => null); + if (this.getTrackedRunId(input.teamName) !== runId) { + return; + } + const expectedMembers = this.resolveOpenCodeRuntimePermissionExpectedMembers({ + teamName: input.teamName, + runId, + laneId: input.laneId, + memberName: input.memberName, + cwd: input.cwd, + previousLaunchState, + }); + const permissionsByMember = this.groupOpenCodeRuntimePermissionsByMember({ + permissions: pendingPermissions, + teamName: input.teamName, + laneId: input.laneId, + memberName: input.memberName, + runId, + sessionId: input.sessionId, + expectedMembers, + previousLaunchState, + }); + if (permissionsByMember.size === 0) { + return; + } + + await this.persistOpenCodeRuntimePendingPermissions({ + ...input, + permissionsByMember, + previousLaunchState, + }); + if (this.getTrackedRunId(input.teamName) !== runId) { + return; + } + this.syncOpenCodeRuntimePermissionSpawnStatuses({ + ...input, + permissionsByMember, + }); + + const members: Record = {}; + for (const [memberName, permissions] of permissionsByMember) { + members[memberName] = this.buildOpenCodePermissionPendingEvidence({ + teamName: input.teamName, + laneId: input.laneId, + memberName, + permissions, + runId, + sessionId: input.sessionId, + previousLaunchState, + }); + } + + this.syncOpenCodeRuntimeToolApprovals({ + teamName: input.teamName, + runId: input.runId, + laneId: input.laneId, + cwd: input.cwd, + members, + expectedMembers, + memberNames: Array.from(permissionsByMember.keys()), + teamColor: input.teamColor, + teamDisplayName: input.teamDisplayName, + }); + } + + private isOpenCodeRuntimePermissionForDeliveryTarget( + input: { + laneId: string; + sessionId?: string | null; + }, + permission: TeamRuntimePendingPermission + ): boolean { + const permissionSessionId = permission.sessionId?.trim(); + const inputSessionId = input.sessionId?.trim(); + if (permissionSessionId && inputSessionId) { + return permissionSessionId === inputSessionId; + } + return true; + } + + private resolveOpenCodeRuntimePermissionExpectedMembers(input: { + teamName: string; + runId: string; + laneId: string; + memberName: string; + cwd: string; + previousLaunchState: PersistedTeamLaunchSnapshot | null; + }): TeamRuntimeMemberSpec[] { + const members = new Map(); + for (const [memberKey, member] of Object.entries(input.previousLaunchState?.members ?? {})) { + if (member.providerId !== 'opencode') continue; + if ((member.laneId?.trim() || 'primary') !== input.laneId) continue; + const memberRunId = member.runtimeRunId?.trim(); + if (memberRunId && memberRunId !== input.runId) continue; + const displayName = this.resolvePersistedLaunchMemberDisplayName(memberKey, member); + members.set(displayName, { + name: displayName, + role: undefined, + workflow: undefined, + isolation: undefined, + providerId: 'opencode', + model: member.model, + effort: member.effort, + cwd: member.cwd?.trim() || input.cwd, + }); + } + + const trackedRunId = this.getTrackedRunId(input.teamName); + const trackedRun = trackedRunId ? this.runs.get(trackedRunId) : null; + for (const member of [ + ...(trackedRun?.allEffectiveMembers ?? []), + ...(trackedRun?.effectiveMembers ?? []), + ]) { + if (member.providerId !== 'opencode' || members.has(member.name)) continue; + const laneIdentity = buildPlannedMemberLaneIdentity({ + leadProviderId: resolveTeamProviderId(trackedRun?.request.providerId), + member: { + name: member.name, + providerId: 'opencode', + }, + }); + if (laneIdentity.laneId !== input.laneId) continue; + members.set(member.name, { + name: member.name, + role: member.role, + workflow: member.workflow, + isolation: member.isolation === 'worktree' ? 'worktree' : undefined, + providerId: 'opencode', + model: member.model, + effort: member.effort, + cwd: member.cwd?.trim() || input.cwd, + }); + } + const runtimeRun = this.runtimeAdapterRunByTeam.get(input.teamName); + if ( + (input.laneId.trim() || 'primary') === 'primary' && + runtimeRun?.runId === input.runId && + runtimeRun.providerId === 'opencode' + ) { + for (const [memberKey, evidence] of Object.entries(runtimeRun.members ?? {})) { + const memberName = evidence.memberName?.trim() || memberKey; + if (!memberName || members.has(memberName)) continue; + members.set(memberName, { + name: memberName, + providerId: 'opencode', + model: evidence.model, + cwd: input.cwd, + }); + } + } + + if (!members.has(input.memberName)) { + members.set(input.memberName, { + name: input.memberName, + providerId: 'opencode', + cwd: input.cwd, + }); + } + return Array.from(members.values()); + } + + private groupOpenCodeRuntimePermissionsByMember(input: { + permissions: readonly TeamRuntimePendingPermission[]; + teamName: string; + laneId: string; + memberName: string; + runId: string; + sessionId?: string | null; + expectedMembers: readonly TeamRuntimeMemberSpec[]; + previousLaunchState: PersistedTeamLaunchSnapshot | null; + }): Map { + const sessionToMember = new Map(); + for (const [memberName, member] of Object.entries(input.previousLaunchState?.members ?? {})) { + if ((member.laneId?.trim() || 'primary') !== input.laneId) continue; + const memberRunId = member.runtimeRunId?.trim(); + if (memberRunId && memberRunId !== input.runId) continue; + const sessionId = member.runtimeSessionId?.trim(); + if (sessionId) { + sessionToMember.set( + sessionId, + this.resolvePersistedLaunchMemberDisplayName(memberName, member) + ); + } + } + const trackedRunId = this.getTrackedRunId(input.teamName); + const trackedRun = trackedRunId ? this.runs.get(trackedRunId) : null; + const lane = trackedRun?.mixedSecondaryLanes?.find( + (candidate) => candidate.laneId === input.laneId + ); + for (const [memberName, evidence] of Object.entries(lane?.result?.members ?? {})) { + const sessionId = evidence.sessionId?.trim(); + if (sessionId) { + sessionToMember.set(sessionId, evidence.memberName?.trim() || memberName); + } + } + const runtimeRun = this.runtimeAdapterRunByTeam.get(input.teamName); + if ( + (input.laneId.trim() || 'primary') === 'primary' && + runtimeRun?.runId === input.runId && + runtimeRun.providerId === 'opencode' + ) { + for (const [memberName, evidence] of Object.entries(runtimeRun.members ?? {})) { + const sessionId = evidence.sessionId?.trim(); + if (sessionId) { + sessionToMember.set(sessionId, evidence.memberName?.trim() || memberName); + } + } + } + + const singleExpectedMember = + input.expectedMembers.length === 1 ? input.expectedMembers[0]?.name : undefined; + const inputSessionId = input.sessionId?.trim(); + const result = new Map(); + for (const permission of input.permissions) { + const permissionSessionId = permission.sessionId?.trim(); + const memberName = permissionSessionId + ? (sessionToMember.get(permissionSessionId) ?? + (inputSessionId === permissionSessionId ? input.memberName : undefined) ?? + singleExpectedMember) + : (singleExpectedMember ?? input.memberName); + if (!memberName) { + continue; + } + result.set(memberName, [...(result.get(memberName) ?? []), permission]); + } + return result; + } + + private buildOpenCodePermissionPendingEvidence(input: { + teamName: string; + laneId: string; + memberName: string; + permissions: readonly TeamRuntimePendingPermission[]; + runId: string; + sessionId?: string | null; + previousLaunchState: PersistedTeamLaunchSnapshot | null; + }): TeamRuntimeMemberLaunchEvidence { + const previous = this.findPersistedLaunchMemberForLane({ + previousLaunchState: input.previousLaunchState, + laneId: input.laneId, + memberName: input.memberName, + runId: input.runId, + })?.member; + const ids = Array.from(new Set(input.permissions.map((permission) => permission.requestId))); + const sessionId = previous?.runtimeSessionId ?? input.sessionId?.trim() ?? undefined; + return { + memberName: input.memberName, + providerId: 'opencode', + ...(previous?.model ? { model: previous.model } : {}), + launchState: + previous?.launchState === 'confirmed_alive' || previous?.bootstrapConfirmed + ? 'confirmed_alive' + : 'runtime_pending_permission', + agentToolAccepted: previous?.agentToolAccepted ?? true, + runtimeAlive: previous?.runtimeAlive ?? false, + bootstrapConfirmed: previous?.bootstrapConfirmed ?? false, + hardFailure: false, + pendingPermissionRequestIds: ids, + pendingApprovals: [...input.permissions], + pendingPermissions: [...input.permissions], + ...(sessionId ? { sessionId } : {}), + livenessKind: previous?.livenessKind ?? 'permission_blocked', + runtimeDiagnostic: 'OpenCode runtime is waiting for permission approval', + runtimeDiagnosticSeverity: 'warning', + diagnostics: [ + 'OpenCode runtime permission request discovered after delivery was blocked.', + ...(previous?.diagnostics ?? []), + ], + }; + } + + private async persistOpenCodeRuntimePendingPermissions(input: { + teamName: string; + runId?: string | null; + laneId: string; + sessionId?: string | null; + permissionsByMember: ReadonlyMap; + previousLaunchState: PersistedTeamLaunchSnapshot | null; + }): Promise { + if (!input.previousLaunchState) { + return; + } + const observedAt = nowIso(); + try { + const changed = await this.enqueueLaunchStateStoreOperation(input.teamName, async () => { + const incomingRunId = input.runId?.trim(); + if (incomingRunId && this.getTrackedRunId(input.teamName) !== incomingRunId) { + return false; + } + const previous = await this.launchStateStore.read(input.teamName).catch(() => null); + if (!previous) { + return false; + } + let didChange = false; + const members = { ...previous.members }; + for (const [memberName, permissions] of input.permissionsByMember) { + const previousEntry = this.findPersistedLaunchMemberForLane({ + previousLaunchState: previous, + laneId: input.laneId, + memberName, + runId: input.runId, + }); + if (!previousEntry || previousEntry.member.providerId !== 'opencode') { + continue; + } + const previousMember = previousEntry.member; + if ((previousMember.laneId?.trim() || 'primary') !== input.laneId) { + continue; + } + const previousRunId = previousMember.runtimeRunId?.trim(); + if (previousRunId && incomingRunId && previousRunId !== incomingRunId) { + continue; + } + const previousSessionId = previousMember.runtimeSessionId?.trim(); + const incomingSessionId = input.sessionId?.trim(); + if (previousSessionId && incomingSessionId && previousSessionId !== incomingSessionId) { + continue; + } + const pendingPermissionRequestIds = Array.from( + new Set(permissions.map((permission) => permission.requestId.trim()).filter(Boolean)) + ); + const nextMember: PersistedTeamLaunchMemberState = { + ...previousMember, + name: memberName, + launchState: + previousMember.launchState === 'confirmed_alive' || previousMember.bootstrapConfirmed + ? 'confirmed_alive' + : 'runtime_pending_permission', + hardFailure: false, + hardFailureReason: undefined, + pendingPermissionRequestIds, + ...(incomingRunId ? { runtimeRunId: incomingRunId } : {}), + ...(incomingSessionId && !previousSessionId + ? { runtimeSessionId: incomingSessionId } + : {}), + livenessKind: previousMember.livenessKind ?? 'permission_blocked', + runtimeDiagnostic: 'OpenCode runtime is waiting for permission approval', + runtimeDiagnosticSeverity: 'warning', + lastEvaluatedAt: observedAt, + diagnostics: mergeRuntimeDiagnostics( + previousMember.diagnostics, + ['waiting for permission approval'], + previousMember.runtimeDiagnostic + ), + }; + if ( + previousMember.name === nextMember.name && + previousMember.launchState === nextMember.launchState && + previousMember.hardFailure === nextMember.hardFailure && + previousMember.hardFailureReason === nextMember.hardFailureReason && + previousMember.pendingPermissionRequestIds?.join('\0') === + nextMember.pendingPermissionRequestIds?.join('\0') && + previousMember.runtimeRunId === nextMember.runtimeRunId && + previousMember.runtimeSessionId === nextMember.runtimeSessionId && + previousMember.livenessKind === nextMember.livenessKind && + previousMember.runtimeDiagnostic === nextMember.runtimeDiagnostic && + previousMember.runtimeDiagnosticSeverity === nextMember.runtimeDiagnosticSeverity + ) { + continue; + } + members[previousEntry.key] = nextMember; + didChange = true; + } + if (!didChange) { + return false; + } + const nextSnapshot = createPersistedLaunchSnapshot({ + teamName: previous.teamName, + expectedMembers: previous.expectedMembers, + bootstrapExpectedMembers: previous.bootstrapExpectedMembers, + leadSessionId: previous.leadSessionId, + launchPhase: previous.launchPhase, + members, + updatedAt: observedAt, + }); + await this.writeLaunchStateSnapshotNow(input.teamName, nextSnapshot); + return true; + }); + if (changed) { + this.invalidateRuntimeSnapshotCaches(input.teamName); + for (const memberName of input.permissionsByMember.keys()) { + this.teamChangeEmitter?.({ + type: 'member-spawn', + teamName: input.teamName, + ...(input.runId ? { runId: input.runId } : {}), + detail: memberName, + }); + } + } + } catch (error) { + logger.debug( + `[${input.teamName}] Failed to persist OpenCode pending runtime permissions: ${getErrorMessage(error)}` + ); + } + } + + private syncOpenCodeRuntimePermissionSpawnStatuses(input: { + teamName: string; + runId?: string | null; + laneId: string; + permissionsByMember: ReadonlyMap; + }): void { + const trackedRunId = this.getTrackedRunId(input.teamName); + const run = trackedRunId ? this.runs.get(trackedRunId) : null; + if (!run || run.runId !== input.runId) { + return; + } + const updatedAt = nowIso(); + for (const [memberName, permissions] of input.permissionsByMember) { + const prev = run.memberSpawnStatuses.get(memberName) ?? createInitialMemberSpawnStatusEntry(); + const lane = run.mixedSecondaryLanes?.find((candidate) => candidate.laneId === input.laneId); + const laneEvidence = lane?.result?.members?.[memberName]; + const pendingPermissionRequestIds = Array.from( + new Set(permissions.map((permission) => permission.requestId.trim()).filter(Boolean)) + ); + const joinedPendingPermissionRequestIds = pendingPermissionRequestIds.join('\0'); + const laneEvidenceNeedsUpdate = Boolean( + lane?.result && + laneEvidence && + (laneEvidence.pendingPermissionRequestIds?.join('\0') !== + joinedPendingPermissionRequestIds || + laneEvidence.runtimeDiagnostic !== + 'OpenCode runtime is waiting for permission approval' || + laneEvidence.runtimeDiagnosticSeverity !== 'warning') + ); + const next: MemberSpawnStatusEntry = { + ...prev, + status: prev.bootstrapConfirmed || laneEvidence?.bootstrapConfirmed ? 'online' : 'waiting', + launchState: prev.launchState, + agentToolAccepted: true, + runtimeAlive: prev.runtimeAlive === true || laneEvidence?.runtimeAlive === true, + bootstrapConfirmed: + prev.bootstrapConfirmed === true || laneEvidence?.bootstrapConfirmed === true, + hardFailure: false, + hardFailureReason: undefined, + error: undefined, + pendingPermissionRequestIds, + livenessKind: prev.livenessKind ?? laneEvidence?.livenessKind ?? 'permission_blocked', + runtimeDiagnostic: 'OpenCode runtime is waiting for permission approval', + runtimeDiagnosticSeverity: 'warning', + updatedAt, + }; + next.launchState = deriveMemberLaunchState(next); + if ( + prev.pendingPermissionRequestIds?.join('\0') === joinedPendingPermissionRequestIds && + prev.launchState === next.launchState && + prev.runtimeDiagnostic === next.runtimeDiagnostic && + !laneEvidenceNeedsUpdate + ) { + continue; + } + run.memberSpawnStatuses.set(memberName, next); + if (lane?.result && laneEvidence) { + lane.result = { + ...lane.result, + members: { + ...lane.result.members, + [memberName]: { + ...laneEvidence, + hardFailure: false, + hardFailureReason: undefined, + pendingPermissionRequestIds, + pendingApprovals: [...permissions], + pendingPermissions: [...permissions], + runtimeDiagnostic: 'OpenCode runtime is waiting for permission approval', + runtimeDiagnosticSeverity: 'warning', + diagnostics: + mergeRuntimeDiagnostics( + laneEvidence.diagnostics, + ['waiting for permission approval'], + laneEvidence.runtimeDiagnostic + ) ?? [], + }, + }, + }; + } + if (this.isCurrentTrackedRun(run)) { + this.emitMemberSpawnChange(run, memberName); + } + } + if (run.isLaunch) { + void this.persistLaunchStateSnapshot(run, run.provisioningComplete ? 'finished' : 'active'); + } + } + private logOpenCodePromptDeliveryEvent( event: string, record: OpenCodePromptDeliveryLedgerRecord, @@ -7678,6 +8337,19 @@ export class TeamProvisioningService { const responseObservation = this.normalizeOpenCodeDeliveryResponseObservation( result.responseObservation ); + await this.maybeSyncOpenCodeRuntimePermissionsAfterDelivery({ + teamName, + runId: runtimeRunId, + laneId: laneIdentity.laneId, + memberName: canonicalMemberName, + cwd, + sessionId: result.sessionId, + responseState: responseObservation?.state, + reason: responseObservation?.reason ?? result.diagnostics[0], + diagnostics: result.diagnostics, + teamColor: config?.color, + teamDisplayName: config?.name, + }); return { delivered: result.ok, accepted: result.ok, @@ -7950,6 +8622,19 @@ export class TeamProvisioningService { const responseObservation = this.normalizeOpenCodeDeliveryResponseObservation( observed.responseObservation ); + await this.maybeSyncOpenCodeRuntimePermissionsAfterDelivery({ + teamName, + runId: runtimeRunId, + laneId: laneIdentity.laneId, + memberName: canonicalMemberName, + cwd, + sessionId: observed.sessionId, + responseState: responseObservation?.state, + reason: responseObservation?.reason ?? observed.diagnostics[0], + diagnostics: observed.diagnostics, + teamColor: config?.color, + teamDisplayName: config?.name, + }); ledgerRecord = await ledger.applyObservation({ id: ledgerRecord.id, responseObservation: responseObservation ?? { @@ -8176,6 +8861,17 @@ export class TeamProvisioningService { }); } catch (error) { const diagnostic = `opencode_message_delivery_exception: ${getErrorMessage(error)}`; + await this.maybeSyncOpenCodeRuntimePermissionsAfterDelivery({ + teamName, + runId: runtimeRunId, + laneId: laneIdentity.laneId, + memberName: canonicalMemberName, + cwd, + reason: diagnostic, + diagnostics: [diagnostic], + teamColor: config?.color, + teamDisplayName: config?.name, + }); if (ledgerRecord && ledger) { ledgerRecord = await ledger.applyDeliveryResult({ id: ledgerRecord.id, @@ -8256,6 +8952,19 @@ export class TeamProvisioningService { const responseObservation = this.normalizeOpenCodeDeliveryResponseObservation( result.responseObservation ); + await this.maybeSyncOpenCodeRuntimePermissionsAfterDelivery({ + teamName, + runId: runtimeRunId, + laneId: laneIdentity.laneId, + memberName: canonicalMemberName, + cwd, + sessionId: result.sessionId, + responseState: responseObservation?.state, + reason: responseObservation?.reason ?? result.diagnostics[0], + diagnostics: result.diagnostics, + teamColor: config?.color, + teamDisplayName: config?.name, + }); const promptAcceptedByRuntimeIdentity = Boolean( result.ok && result.runtimePromptMessageId?.trim() ); @@ -28752,6 +29461,8 @@ export class TeamProvisioningService { current.firstSpawnAcceptedAt != null ? Date.parse(current.firstSpawnAcceptedAt) : NaN; const initialFailureReason = current.hardFailureReason ?? current.runtimeDiagnostic; const hadAutoClearableFailure = isAutoClearableLaunchFailureReason(initialFailureReason); + const requiresConfirmedBootstrapToClearFailure = + isCliProvisionedButNotAliveFailureReason(initialFailureReason); current.runtimeAlive = observedRuntimeAlive; current.lastRuntimeAliveAt = observedRuntimeAlive ? now : current.lastRuntimeAliveAt; current.livenessKind = runtimeMetadata?.[1].livenessKind; @@ -28775,6 +29486,7 @@ export class TeamProvisioningService { current.agentToolAccepted === true || typeof current.firstSpawnAcceptedAt === 'string'; if ( hadAutoClearableFailure && + !requiresConfirmedBootstrapToClearFailure && (bootstrapProvesSpawnAcceptance || currentProvesSpawnAcceptance) ) { current.hardFailure = false; @@ -31445,6 +32157,7 @@ export class TeamProvisioningService { cwd: string; members: Record; expectedMembers: TeamRuntimeMemberSpec[]; + memberNames?: readonly string[]; teamColor?: string; teamDisplayName?: string; }): void { @@ -31454,6 +32167,7 @@ export class TeamProvisioningService { teamName: input.teamName, runId: input.runId, laneId: input.laneId, + memberNames: input.memberNames, providerId: 'opencode', }, entries diff --git a/src/main/services/team/approvals/RuntimeToolApprovalCoordinator.ts b/src/main/services/team/approvals/RuntimeToolApprovalCoordinator.ts index eb921f39..d69a42c4 100644 --- a/src/main/services/team/approvals/RuntimeToolApprovalCoordinator.ts +++ b/src/main/services/team/approvals/RuntimeToolApprovalCoordinator.ts @@ -64,6 +64,7 @@ export interface RuntimeToolApprovalSyncScope { teamName: string; runId: string; laneId?: string; + memberNames?: readonly string[]; providerId?: RuntimeApprovalProviderId; } @@ -405,6 +406,9 @@ export class RuntimeToolApprovalCoordinator { if (scope.laneId && entry.laneId !== scope.laneId) { return false; } + if (scope.memberNames?.length && !scope.memberNames.includes(entry.memberName)) { + return false; + } if (scope.providerId && entry.providerId !== scope.providerId) { return false; } diff --git a/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts b/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts index 7c9c36c5..dc898ffb 100644 --- a/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts +++ b/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts @@ -160,11 +160,14 @@ export interface OpenCodeListRuntimePermissionsCommandBody { teamId: string; teamName: string; laneId?: string; + memberName?: string; + sessionId?: string | null; projectPath?: string; } export interface OpenCodeListRuntimePermissionsCommandData { permissions: OpenCodeRuntimePermissionCommandData[]; + diagnostics?: string[]; } export interface OpenCodeCleanupHostsCommandBody { diff --git a/src/main/services/team/opencode/bridge/OpenCodeReadinessBridge.ts b/src/main/services/team/opencode/bridge/OpenCodeReadinessBridge.ts index fd67f0fa..3568e35e 100644 --- a/src/main/services/team/opencode/bridge/OpenCodeReadinessBridge.ts +++ b/src/main/services/team/opencode/bridge/OpenCodeReadinessBridge.ts @@ -257,7 +257,13 @@ export class OpenCodeReadinessBridge implements OpenCodeTeamRuntimeBridgePort { if (result.ok) { return result.data; } - return { permissions: [] }; + return { + permissions: [], + diagnostics: [ + `OpenCode runtime permission list bridge failed: ${result.error.kind}: ${result.error.message}`, + ...result.diagnostics.map(formatDiagnosticEvent), + ], + }; } async cleanupOpenCodeHosts( diff --git a/src/main/services/team/provisioning/TeamProvisioningLaunchFailurePolicy.ts b/src/main/services/team/provisioning/TeamProvisioningLaunchFailurePolicy.ts index 58165db5..2f6366c0 100644 --- a/src/main/services/team/provisioning/TeamProvisioningLaunchFailurePolicy.ts +++ b/src/main/services/team/provisioning/TeamProvisioningLaunchFailurePolicy.ts @@ -37,6 +37,16 @@ export function isProcessTableUnavailableFailureReason(reason?: string): boolean ); } +export function isCliProvisionedButNotAliveFailureReason(reason?: string): boolean { + const text = reason?.trim(); + if (!text) { + return false; + } + return /^CLI process exited \(code (?:unknown|\d+|\?)\) [\u2014-] team provisioned but not alive$/i.test( + text + ); +} + export function stripProcessTableUnavailableDiagnosticSuffix(reason: string): string | null { const match = /^(.*?);\s*process table (?:is )?unavailable$/i.exec(reason.trim()); const baseReason = match?.[1]?.trim(); @@ -53,7 +63,8 @@ function isBaseAutoClearableLaunchFailureReason(reason?: string): boolean { isBootstrapMcpResourceReadFailureReason(reason) || isBootstrapCheckInTimeoutFailureReason(reason) || isBootstrapInstructionPromptFailureReason(reason) || - isLaunchCleanupBootstrapIncompleteFailureReason(reason) + isLaunchCleanupBootstrapIncompleteFailureReason(reason) || + isCliProvisionedButNotAliveFailureReason(reason) ); } diff --git a/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts b/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts index fe77dbf5..e986e5c3 100644 --- a/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts +++ b/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts @@ -5,6 +5,8 @@ import type { OpenCodeBridgeRuntimeSnapshot, OpenCodeLaunchTeamCommandBody, OpenCodeLaunchTeamCommandData, + OpenCodeListRuntimePermissionsCommandBody, + OpenCodeListRuntimePermissionsCommandData, OpenCodeObserveMessageDeliveryCommandBody, OpenCodeObserveMessageDeliveryCommandData, OpenCodeReconcileTeamCommandBody, @@ -24,6 +26,8 @@ import type { TeamRuntimeMemberStopEvidence, TeamRuntimePendingPermission, TeamRuntimePermissionAnswerInput, + TeamRuntimePermissionListInput, + TeamRuntimePermissionListResult, TeamRuntimePrepareResult, TeamRuntimeReconcileInput, TeamRuntimeReconcileResult, @@ -59,6 +63,9 @@ export interface OpenCodeTeamRuntimeBridgePort { answerOpenCodeRuntimePermission?( input: OpenCodeAnswerPermissionCommandBody ): Promise; + listOpenCodeRuntimePermissions?( + input: OpenCodeListRuntimePermissionsCommandBody + ): Promise; } export interface OpenCodeTeamRuntimeMessageInput { @@ -599,6 +606,30 @@ export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter { ); } + async listRuntimePermissions( + input: TeamRuntimePermissionListInput + ): Promise { + if (!this.bridge.listOpenCodeRuntimePermissions) { + return { + permissions: [], + diagnostics: ['OpenCode runtime permission list bridge is not registered.'], + }; + } + + const data = await this.bridge.listOpenCodeRuntimePermissions({ + teamId: input.teamName, + teamName: input.teamName, + laneId: input.laneId, + memberName: input.memberName, + sessionId: input.sessionId, + projectPath: input.cwd, + }); + return { + permissions: normalizeOpenCodeRuntimePendingPermissions(data.permissions) ?? [], + diagnostics: data.diagnostics ?? [], + }; + } + async stop(input: TeamRuntimeStopInput): Promise { if (this.bridge.stopOpenCodeTeam) { const projectPath = input.cwd ?? this.lastProjectPathByTeamName.get(input.teamName); diff --git a/src/main/services/team/runtime/TeamRuntimeAdapter.ts b/src/main/services/team/runtime/TeamRuntimeAdapter.ts index 22a2d909..d048f88e 100644 --- a/src/main/services/team/runtime/TeamRuntimeAdapter.ts +++ b/src/main/services/team/runtime/TeamRuntimeAdapter.ts @@ -56,6 +56,19 @@ export interface TeamRuntimePermissionAnswerInput { previousLaunchState: PersistedTeamLaunchSnapshot | null; } +export interface TeamRuntimePermissionListInput { + teamName: string; + laneId?: string; + cwd?: string; + memberName?: string; + sessionId?: string | null; +} + +export interface TeamRuntimePermissionListResult { + permissions: TeamRuntimePendingPermission[]; + diagnostics: string[]; +} + export interface TeamRuntimeLaunchInput { runId: string; teamName: string; @@ -206,6 +219,9 @@ export interface TeamLaunchRuntimeAdapter { answerRuntimePermission?( input: TeamRuntimePermissionAnswerInput ): Promise; + listRuntimePermissions?( + input: TeamRuntimePermissionListInput + ): Promise; } export function isTeamRuntimeProviderId(value: unknown): value is TeamRuntimeProviderId { diff --git a/src/main/services/team/runtime/index.ts b/src/main/services/team/runtime/index.ts index bbf41d8f..4baec494 100644 --- a/src/main/services/team/runtime/index.ts +++ b/src/main/services/team/runtime/index.ts @@ -14,6 +14,8 @@ export type { TeamRuntimeMemberStopEvidence, TeamRuntimePendingApproval, TeamRuntimePendingPermission, + TeamRuntimePermissionListInput, + TeamRuntimePermissionListResult, TeamRuntimePrepareFailure, TeamRuntimePrepareResult, TeamRuntimePrepareSuccess, diff --git a/test/main/services/team/OpenCodeReadinessBridge.test.ts b/test/main/services/team/OpenCodeReadinessBridge.test.ts index c73e2ff5..fbc480bc 100644 --- a/test/main/services/team/OpenCodeReadinessBridge.test.ts +++ b/test/main/services/team/OpenCodeReadinessBridge.test.ts @@ -202,6 +202,46 @@ describe('OpenCodeReadinessBridge', () => { ); }); + it('preserves diagnostics when runtime permission listing bridge fails', async () => { + const executor = fakeExecutor( + bridgeCommandFailure({ + command: 'opencode.listRuntimePermissions', + requestId: 'permission-list-req-1', + kind: 'timeout', + message: 'permission list timed out', + }) + ); + const bridge = new OpenCodeReadinessBridge(executor); + + await expect( + bridge.listOpenCodeRuntimePermissions({ + teamId: 'team-a', + teamName: 'team-a', + laneId: 'primary', + projectPath: '/repo', + }) + ).resolves.toEqual({ + permissions: [], + diagnostics: [ + 'OpenCode runtime permission list bridge failed: timeout: permission list timed out', + ], + }); + + expect(executor.execute).toHaveBeenCalledWith( + 'opencode.listRuntimePermissions', + { + teamId: 'team-a', + teamName: 'team-a', + laneId: 'primary', + projectPath: '/repo', + }, + { + cwd: '/repo', + timeoutMs: 30_000, + } + ); + }); + it('gives observeMessageDelivery enough time for OpenCode plain-text fallback reconciliation', async () => { const executor = fakeExecutor( bridgeCommandSuccess({ diff --git a/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts b/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts index 50e98f0d..41d5f240 100644 --- a/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts +++ b/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts @@ -1586,6 +1586,91 @@ describe('OpenCodeTeamRuntimeAdapter', () => { ).rejects.toThrow('OpenCode permission answer bridge is not registered.'); }); + it('lists OpenCode runtime permissions through the bridge', async () => { + const listOpenCodeRuntimePermissions = vi.fn< + NonNullable + >(async () => ({ + permissions: [ + { + requestId: 'perm-1', + sessionId: 'session-alice', + tool: 'bash', + title: 'Run git status', + kind: 'tool', + raw: { patterns: ['git status'] }, + }, + { + requestId: 'perm-1', + sessionId: 'session-alice', + tool: 'bash', + title: 'Duplicate', + kind: 'tool', + }, + { + requestId: ' ', + sessionId: null, + tool: null, + title: null, + kind: null, + }, + ], + diagnostics: ['permission list recovered from bridge warning'], + })); + const adapter = new OpenCodeTeamRuntimeAdapter( + bridgePort(readiness({ state: 'ready', launchAllowed: true }), { + listOpenCodeRuntimePermissions, + }) + ); + + await expect( + adapter.listRuntimePermissions({ + teamName: 'team-a', + laneId: 'secondary:opencode:alice', + memberName: 'alice', + sessionId: 'session-alice', + cwd: '/repo', + }) + ).resolves.toEqual({ + permissions: [ + { + providerId: 'opencode', + requestId: 'perm-1', + sessionId: 'session-alice', + tool: 'bash', + title: 'Run git status', + kind: 'tool', + raw: { patterns: ['git status'] }, + }, + ], + diagnostics: ['permission list recovered from bridge warning'], + }); + expect(listOpenCodeRuntimePermissions).toHaveBeenCalledWith({ + teamId: 'team-a', + teamName: 'team-a', + laneId: 'secondary:opencode:alice', + memberName: 'alice', + sessionId: 'session-alice', + projectPath: '/repo', + }); + }); + + it('returns a diagnostic when the OpenCode runtime permission list bridge is unavailable', async () => { + const adapter = new OpenCodeTeamRuntimeAdapter( + bridgePort(readiness({ state: 'ready', launchAllowed: true })) + ); + + await expect( + adapter.listRuntimePermissions({ + teamName: 'team-a', + laneId: 'primary', + cwd: '/repo', + }) + ).resolves.toEqual({ + permissions: [], + diagnostics: ['OpenCode runtime permission list bridge is not registered.'], + }); + }); + it('does not mark created bridge members without runtimePid as runtimeAlive', async () => { const launchOpenCodeTeam = vi.fn( async () => diff --git a/test/main/services/team/RuntimeToolApprovalCoordinator.test.ts b/test/main/services/team/RuntimeToolApprovalCoordinator.test.ts index 9f6f5107..0eb561a8 100644 --- a/test/main/services/team/RuntimeToolApprovalCoordinator.test.ts +++ b/test/main/services/team/RuntimeToolApprovalCoordinator.test.ts @@ -190,6 +190,48 @@ describe('RuntimeToolApprovalCoordinator', () => { }); }); + it('keeps other member approvals when runtime sync is scoped to one member', () => { + const alice = approvalEntry(); + const bob = approvalEntry({ + providerRequestId: 'perm-bob', + memberName: 'bob', + approval: { + requestId: 'opencode:run-1:perm-bob', + runId: 'run-1', + teamName: 'team-a', + providerId: 'opencode', + source: 'bob', + toolName: 'Bash', + toolInput: { command: 'pnpm test' }, + receivedAt: '2026-05-22T10:00:00.000Z', + runtimePermission: { + providerId: 'opencode', + laneId: 'primary', + memberName: 'bob', + providerRequestId: 'perm-bob', + sessionId: 'ses-bob', + }, + }, + }); + coordinator.sync({ teamName: 'team-a', runId: 'run-1', laneId: 'primary' }, [alice, bob]); + + coordinator.sync( + { teamName: 'team-a', runId: 'run-1', laneId: 'primary', memberNames: ['alice'] }, + [alice] + ); + + expect(coordinator.get('team-a', 'opencode:run-1:perm-bob')).toBe(bob); + expect(coordinator.size('team-a')).toBe(2); + expect( + events.some( + (event) => + 'autoResolved' in event && + event.requestId === 'opencode:run-1:perm-bob' && + event.reason === 'runtime_resolved' + ) + ).toBe(false); + }); + it('rejects stale UI responses by run id', async () => { coordinator.sync({ teamName: 'team-a', runId: 'run-1', laneId: 'primary' }, [approvalEntry()]); diff --git a/test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts b/test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts index 47bad9c3..904aeddd 100644 --- a/test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts +++ b/test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts @@ -26,7 +26,10 @@ import { type TeamRuntimeLaunchResult, type TeamRuntimeMemberLaunchEvidence, type TeamRuntimeMemberSpec, + type TeamRuntimePendingPermission, type TeamRuntimePermissionAnswerInput, + type TeamRuntimePermissionListInput, + type TeamRuntimePermissionListResult, type TeamRuntimePrepareResult, type TeamRuntimeReconcileInput, type TeamRuntimeReconcileResult, @@ -10368,6 +10371,222 @@ describe('Team agent launch matrix safe e2e', () => { expect(adapter.messageInputs[0]?.runId).not.toBe(staleRun.runId); }); + it('surfaces mixed OpenCode side-lane delivery permission blocks through shared approvals', async () => { + const teamName = 'mixed-opencode-delivery-permission-approval-safe-e2e'; + await writeMixedTeamConfig({ + teamName, + projectPath, + includeGeminiPrimary: true, + primaryProviderId: 'anthropic', + }); + await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); + await writeMembersMeta(teamName, { + includeGeminiPrimary: true, + primaryProviderId: 'anthropic', + }); + const adapter = new PermissionBlockedOpenCodeRuntimeAdapter(); + adapter.setRuntimePermissions('secondary:opencode:bob', [ + { + providerId: 'opencode', + requestId: 'perm-bob-delivery', + sessionId: 'session-bob', + tool: 'bash', + title: 'Run pnpm test', + kind: 'tool', + raw: { patterns: ['pnpm test'] }, + }, + ]); + const svc = new TeamProvisioningService(); + svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); + const approvalEvents: ToolApprovalEvent[] = []; + svc.setToolApprovalEventEmitter((event) => approvalEvents.push(event)); + const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); + addGeminiPrimaryToMixedRun(run); + run.runId = `run-${teamName}-current`; + await markMixedOpenCodeLaneConfirmedForTest(run, 'bob'); + trackLiveRun(svc, run); + + await expect( + svc.deliverOpenCodeMemberMessage(teamName, { + memberName: 'bob', + text: 'trigger a side-lane permission-blocked delivery', + messageId: 'msg-mixed-opencode-permission-blocked', + }) + ).resolves.toMatchObject({ + delivered: false, + responseState: 'permission_blocked', + }); + + expect(adapter.permissionListInputs).toEqual([ + { + teamName, + laneId: 'secondary:opencode:bob', + cwd: projectPath, + memberName: 'bob', + sessionId: 'session-bob', + }, + ]); + const approval = approvalEvents.find( + (event): event is ToolApprovalRequest => + !('dismissed' in event) && !('autoResolved' in event) + ); + expect(approval).toMatchObject({ + requestId: `opencode:${run.runId}:perm-bob-delivery`, + runId: run.runId, + teamName, + providerId: 'opencode', + source: 'bob', + toolName: 'Bash', + toolInput: { + provider: 'opencode', + providerRequestId: 'perm-bob-delivery', + command: 'pnpm test', + }, + runtimePermission: { + providerId: 'opencode', + laneId: 'secondary:opencode:bob', + memberName: 'bob', + providerRequestId: 'perm-bob-delivery', + }, + }); + expect(run.memberSpawnStatuses.get('bob')).toMatchObject({ + pendingPermissionRequestIds: ['perm-bob-delivery'], + runtimeDiagnostic: 'OpenCode runtime is waiting for permission approval', + }); + await waitForCondition(async () => { + let persisted: { members?: Record }; + try { + persisted = JSON.parse( + await fs.readFile(path.join(getTeamsBasePath(), teamName, 'launch-state.json'), 'utf8') + ) as typeof persisted; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return false; + } + throw error; + } + return persisted.members?.bob?.pendingPermissionRequestIds?.includes('perm-bob-delivery') === true; + }); + }); + + it('persists mixed OpenCode permissions to the matching lane member when persisted keys diverge', async () => { + const teamName = 'mixed-opencode-delivery-permission-lane-key-safe-e2e'; + await writeMixedTeamConfig({ + teamName, + projectPath, + includeGeminiPrimary: true, + primaryProviderId: 'anthropic', + }); + await writeTeamMeta(teamName, projectPath, { primaryProviderId: 'anthropic' }); + await writeMembersMeta(teamName, { + includeGeminiPrimary: true, + primaryProviderId: 'anthropic', + }); + const adapter = new PermissionBlockedOpenCodeRuntimeAdapter(); + adapter.setRuntimePermissions('secondary:opencode:bob', [ + { + providerId: 'opencode', + requestId: 'perm-bob-lane-key', + sessionId: 'session-bob', + tool: 'bash', + title: 'Run git status', + kind: 'tool', + raw: { patterns: ['git status'] }, + }, + ]); + const svc = new TeamProvisioningService(); + svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); + const approvalEvents: ToolApprovalEvent[] = []; + svc.setToolApprovalEventEmitter((event) => approvalEvents.push(event)); + const run = createMixedLiveRun({ teamName, projectPath, primaryProviderId: 'anthropic' }); + addGeminiPrimaryToMixedRun(run); + run.runId = `run-${teamName}-current`; + run.isLaunch = false; + await markMixedOpenCodeLaneConfirmedForTest(run, 'bob'); + trackLiveRun(svc, run); + + const teamDir = path.join(getTeamsBasePath(), teamName); + await fs.mkdir(teamDir, { recursive: true }); + await fs.writeFile( + path.join(teamDir, 'launch-state.json'), + `${JSON.stringify( + createPersistedLaunchSnapshot({ + teamName, + expectedMembers: ['alice', 'reviewer', 'bob'], + leadSessionId: 'lead-session', + launchPhase: 'active', + members: { + bob: { + name: 'bob', + providerId: 'opencode', + model: 'opencode/old-primary', + laneId: 'primary', + laneKind: 'primary', + laneOwnerProviderId: 'anthropic', + launchState: 'confirmed_alive', + agentToolAccepted: true, + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + runtimeRunId: run.runId, + runtimeSessionId: 'session-primary-bob', + lastEvaluatedAt: '2026-04-23T10:00:00.000Z', + }, + 'secondary:opencode:bob': { + name: 'bob', + providerId: 'opencode', + model: 'opencode/minimax-m2.5-free', + laneId: 'secondary:opencode:bob', + laneKind: 'secondary', + laneOwnerProviderId: 'opencode', + launchState: 'confirmed_alive', + agentToolAccepted: true, + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + runtimeRunId: run.runId, + runtimeSessionId: 'session-bob', + livenessKind: 'confirmed_bootstrap', + lastEvaluatedAt: '2026-04-23T10:00:00.000Z', + }, + }, + }) + )}\n`, + 'utf8' + ); + + await expect( + svc.deliverOpenCodeMemberMessage(teamName, { + memberName: 'bob', + text: 'trigger a side-lane permission-blocked delivery with lane-keyed state', + messageId: 'msg-mixed-opencode-permission-lane-key', + }) + ).resolves.toMatchObject({ + delivered: false, + responseState: 'permission_blocked', + }); + + const approvals = approvalEvents.filter( + (event): event is ToolApprovalRequest => + !('dismissed' in event) && !('autoResolved' in event) + ); + expect(approvals).toEqual([ + expect.objectContaining({ + requestId: `opencode:${run.runId}:perm-bob-lane-key`, + source: 'bob', + }), + ]); + const persisted = JSON.parse( + await fs.readFile(path.join(teamDir, 'launch-state.json'), 'utf8') + ) as { + members?: Record; + }; + expect(persisted.members?.bob?.pendingPermissionRequestIds).toBeUndefined(); + expect( + persisted.members?.['secondary:opencode:bob']?.pendingPermissionRequestIds + ).toEqual(['perm-bob-lane-key']); + }); + it('refreshes stale mixed OpenCode secondary session evidence before direct delivery when MCP transport changed', async () => { const teamName = 'mixed-opencode-secondary-transport-refresh-safe-e2e'; await writeMixedTeamConfig({ @@ -10863,6 +11082,557 @@ describe('Team agent launch matrix safe e2e', () => { expect(adapter.messageInputs[0]?.runId).not.toBe(first.runId); }); + it('surfaces pure OpenCode delivery permission blocks as the shared tool approval dialog', async () => { + const teamName = 'pure-opencode-delivery-permission-approval-safe-e2e'; + const adapter = new PermissionBlockedOpenCodeRuntimeAdapter(); + adapter.setRuntimePermissions('primary', [ + { + providerId: 'opencode', + requestId: 'perm-alice-delivery', + sessionId: null, + tool: 'bash', + title: 'Run git status', + kind: 'tool', + raw: { patterns: ['git status'] }, + }, + ]); + const svc = new TeamProvisioningService(); + svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); + const approvalEvents: ToolApprovalEvent[] = []; + svc.setToolApprovalEventEmitter((event) => approvalEvents.push(event)); + + const launch = await svc.createTeam( + { + teamName, + cwd: projectPath, + providerId: 'opencode', + model: 'opencode/big-pickle', + skipPermissions: true, + members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], + }, + () => undefined + ); + + await expect( + svc.deliverOpenCodeMemberMessage(teamName, { + memberName: 'alice', + text: 'trigger a permission-blocked delivery', + messageId: 'msg-pure-opencode-permission-blocked', + }) + ).resolves.toMatchObject({ + delivered: false, + responseState: 'permission_blocked', + }); + + expect(adapter.permissionListInputs).toEqual([ + { + teamName, + laneId: 'primary', + cwd: projectPath, + memberName: 'alice', + sessionId: 'session-alice', + }, + ]); + const approval = approvalEvents.find( + (event): event is ToolApprovalRequest => + !('dismissed' in event) && !('autoResolved' in event) + ); + expect(approval).toMatchObject({ + requestId: `opencode:${launch.runId}:perm-alice-delivery`, + runId: launch.runId, + teamName, + providerId: 'opencode', + source: 'alice', + toolName: 'Bash', + toolInput: { + provider: 'opencode', + providerRequestId: 'perm-alice-delivery', + command: 'git status', + }, + runtimePermission: { + providerId: 'opencode', + laneId: 'primary', + memberName: 'alice', + providerRequestId: 'perm-alice-delivery', + }, + }); + const statuses = await svc.getMemberSpawnStatuses(teamName); + expect(statuses.statuses.alice?.pendingPermissionRequestIds).toEqual([ + 'perm-alice-delivery', + ]); + }); + + it('keeps other primary OpenCode approvals when a delivery-blocked member syncs permissions', async () => { + const teamName = 'pure-opencode-delivery-permission-member-scope-safe-e2e'; + const adapter = new PermissionBlockedOpenCodeRuntimeAdapter('partial_pending', { + alice: 'confirmed', + bob: 'permission', + }); + adapter.setRuntimePermissions('primary', [ + { + providerId: 'opencode', + requestId: 'perm-alice-delivery', + sessionId: 'session-alice', + tool: 'bash', + title: 'Run git status', + kind: 'tool', + raw: { patterns: ['git status'] }, + }, + ]); + const svc = new TeamProvisioningService(); + svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); + const approvalEvents: ToolApprovalEvent[] = []; + svc.setToolApprovalEventEmitter((event) => approvalEvents.push(event)); + + const launch = await svc.createTeam( + { + teamName, + cwd: projectPath, + providerId: 'opencode', + model: 'opencode/big-pickle', + skipPermissions: false, + members: [ + { name: 'alice', role: 'Developer', providerId: 'opencode' }, + { name: 'bob', role: 'Reviewer', providerId: 'opencode' }, + ], + }, + () => undefined + ); + + expect(approvalEvents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + requestId: `opencode:${launch.runId}:perm-bob`, + source: 'bob', + }), + ]) + ); + approvalEvents.length = 0; + + await expect( + svc.deliverOpenCodeMemberMessage(teamName, { + memberName: 'alice', + text: 'trigger alice permission-blocked delivery', + messageId: 'msg-pure-opencode-permission-blocked-member-scope', + }) + ).resolves.toMatchObject({ + delivered: false, + responseState: 'permission_blocked', + }); + + expect(approvalEvents).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + requestId: `opencode:${launch.runId}:perm-alice-delivery`, + source: 'alice', + }), + ]) + ); + expect(approvalEvents).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + autoResolved: true, + requestId: `opencode:${launch.runId}:perm-bob`, + reason: 'runtime_resolved', + }), + ]) + ); + + await svc.respondToToolApproval(teamName, launch.runId!, `opencode:${launch.runId}:perm-bob`, true); + expect(adapter.permissionAnswerInputs).toEqual([ + expect.objectContaining({ + runId: launch.runId, + teamName, + laneId: 'primary', + memberName: 'bob', + requestId: 'perm-bob', + decision: 'allow', + }), + ]); + }); + + it('does not surface stale OpenCode delivery permissions after the tracked run changes during listing', async () => { + const teamName = 'pure-opencode-delivery-permission-stale-run-safe-e2e'; + const adapter = new PermissionBlockedOpenCodeRuntimeAdapter(); + adapter.setRuntimePermissions('primary', [ + { + providerId: 'opencode', + requestId: 'perm-alice-stale-run', + sessionId: 'session-alice', + tool: 'bash', + title: 'Run git status', + kind: 'tool', + raw: { patterns: ['git status'] }, + }, + ]); + let releaseList!: () => void; + let markListStarted!: () => void; + const listStarted = new Promise((resolve) => { + markListStarted = resolve; + }); + const releaseListPromise = new Promise((resolve) => { + releaseList = resolve; + }); + const originalListRuntimePermissions = adapter.listRuntimePermissions.bind(adapter); + vi.spyOn(adapter, 'listRuntimePermissions').mockImplementation(async (input) => { + markListStarted(); + await releaseListPromise; + return originalListRuntimePermissions(input); + }); + + const svc = new TeamProvisioningService(); + svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); + const approvalEvents: ToolApprovalEvent[] = []; + svc.setToolApprovalEventEmitter((event) => approvalEvents.push(event)); + + const launch = await svc.createTeam( + { + teamName, + cwd: projectPath, + providerId: 'opencode', + model: 'opencode/big-pickle', + skipPermissions: true, + members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], + }, + () => undefined + ); + + const delivery = svc.deliverOpenCodeMemberMessage(teamName, { + memberName: 'alice', + text: 'trigger permission listing while the run changes', + messageId: 'msg-pure-opencode-permission-stale-run', + }); + await listStarted; + (svc as any).provisioningRunByTeam.set(teamName, `${launch.runId}-replacement`); + (svc as any).aliveRunByTeam.set(teamName, `${launch.runId}-replacement`); + releaseList(); + + await expect(delivery).resolves.toMatchObject({ + delivered: false, + responseState: 'permission_blocked', + }); + expect(approvalEvents).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + requestId: `opencode:${launch.runId}:perm-alice-stale-run`, + }), + ]) + ); + }); + + it('does not surface stale OpenCode delivery permissions after the tracked run changes during persisted-state read', async () => { + const teamName = 'pure-opencode-delivery-permission-stale-read-safe-e2e'; + const adapter = new PermissionBlockedOpenCodeRuntimeAdapter(); + adapter.setRuntimePermissions('primary', [ + { + providerId: 'opencode', + requestId: 'perm-alice-stale-read', + sessionId: 'session-alice', + tool: 'bash', + title: 'Run git status', + kind: 'tool', + raw: { patterns: ['git status'] }, + }, + ]); + const originalListRuntimePermissions = adapter.listRuntimePermissions.bind(adapter); + let replaceRunOnNextLaunchStateRead = false; + vi.spyOn(adapter, 'listRuntimePermissions').mockImplementation(async (input) => { + const result = await originalListRuntimePermissions(input); + replaceRunOnNextLaunchStateRead = true; + return result; + }); + + const svc = new TeamProvisioningService(); + svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); + const approvalEvents: ToolApprovalEvent[] = []; + svc.setToolApprovalEventEmitter((event) => approvalEvents.push(event)); + + const launch = await svc.createTeam( + { + teamName, + cwd: projectPath, + providerId: 'opencode', + model: 'opencode/big-pickle', + skipPermissions: true, + members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], + }, + () => undefined + ); + const launchStateStore = (svc as any).launchStateStore as { + read(teamName: string): Promise; + }; + const originalRead = launchStateStore.read.bind(launchStateStore); + vi.spyOn(launchStateStore, 'read').mockImplementation(async (readTeamName) => { + const snapshot = await originalRead(readTeamName); + if (replaceRunOnNextLaunchStateRead && readTeamName === teamName) { + replaceRunOnNextLaunchStateRead = false; + (svc as any).provisioningRunByTeam.set(teamName, `${launch.runId}-replacement`); + (svc as any).aliveRunByTeam.set(teamName, `${launch.runId}-replacement`); + } + return snapshot; + }); + + await expect( + svc.deliverOpenCodeMemberMessage(teamName, { + memberName: 'alice', + text: 'trigger permission listing before the persisted state read changes run', + messageId: 'msg-pure-opencode-permission-stale-read', + }) + ).resolves.toMatchObject({ + delivered: false, + responseState: 'permission_blocked', + }); + expect(approvalEvents).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + requestId: `opencode:${launch.runId}:perm-alice-stale-read`, + }), + ]) + ); + }); + + it('surfaces OpenCode permissions when inline delivery observe hits a pending request', async () => { + const teamName = 'pure-opencode-inline-observe-permission-approval-safe-e2e'; + const adapter = new PermissionBlockedInlineObserveOpenCodeRuntimeAdapter(); + adapter.setRuntimePermissions('primary', [ + { + providerId: 'opencode', + requestId: 'perm-alice-inline-observe', + sessionId: 'session-alice', + tool: 'bash', + title: 'Run printf', + kind: 'tool', + raw: { patterns: ['printf inline-observe'] }, + }, + ]); + const svc = new TeamProvisioningService(); + svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); + const approvalEvents: ToolApprovalEvent[] = []; + svc.setToolApprovalEventEmitter((event) => approvalEvents.push(event)); + + const launch = await svc.createTeam( + { + teamName, + cwd: projectPath, + providerId: 'opencode', + model: 'opencode/big-pickle', + skipPermissions: true, + members: [{ name: 'alice', role: 'Developer', providerId: 'opencode' }], + }, + () => undefined + ); + + await expect( + svc.deliverOpenCodeMemberMessage(teamName, { + memberName: 'alice', + text: 'trigger inline observe permission block', + messageId: 'msg-pure-opencode-inline-observe-permission-blocked', + replyRecipient: 'user', + actionMode: 'ask', + source: 'watcher', + inboxTimestamp: '2026-05-08T10:00:00.000Z', + }) + ).resolves.toMatchObject({ + delivered: true, + accepted: true, + responsePending: true, + responseState: 'reconcile_failed', + }); + + expect(adapter.permissionListInputs).toEqual([ + { + teamName, + laneId: 'primary', + cwd: projectPath, + memberName: 'alice', + sessionId: 'session-alice', + }, + ]); + expect(adapter.observeInputs).toHaveLength(1); + expect(adapter.observeInputs[0]).toMatchObject({ + sessionId: 'session-alice', + runtimePromptMessageId: 'prompt-msg-pure-opencode-inline-observe-permission-blocked', + prePromptCursor: 'cursor-before-inline-observe-permission', + }); + const approval = approvalEvents.find( + (event): event is ToolApprovalRequest => + !('dismissed' in event) && !('autoResolved' in event) + ); + expect(approval).toMatchObject({ + requestId: `opencode:${launch.runId}:perm-alice-inline-observe`, + runId: launch.runId, + teamName, + providerId: 'opencode', + source: 'alice', + toolName: 'Bash', + runtimePermission: { + providerId: 'opencode', + laneId: 'primary', + memberName: 'alice', + providerRequestId: 'perm-alice-inline-observe', + sessionId: 'session-alice', + }, + }); + }); + + it('does not assign unknown primary-lane OpenCode permission sessions to the delivery target', async () => { + const teamName = 'pure-opencode-primary-permission-session-scope-safe-e2e'; + const adapter = new PermissionBlockedWithoutSessionOpenCodeRuntimeAdapter(); + adapter.setRuntimePermissions('primary', [ + { + providerId: 'opencode', + requestId: 'perm-alice-delivery', + sessionId: 'session-alice', + tool: 'bash', + title: 'Run git status', + kind: 'tool', + raw: { patterns: ['git status'] }, + }, + { + providerId: 'opencode', + requestId: 'perm-unknown-session', + sessionId: 'session-charlie', + tool: 'bash', + title: 'Run npm test', + kind: 'tool', + raw: { patterns: ['npm test'] }, + }, + ]); + const svc = new TeamProvisioningService(); + svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); + const approvalEvents: ToolApprovalEvent[] = []; + svc.setToolApprovalEventEmitter((event) => approvalEvents.push(event)); + + const launch = await svc.createTeam( + { + teamName, + cwd: projectPath, + providerId: 'opencode', + model: 'opencode/big-pickle', + skipPermissions: true, + members: [ + { name: 'alice', role: 'Developer', providerId: 'opencode' }, + { name: 'bob', role: 'Reviewer', providerId: 'opencode' }, + ], + }, + () => undefined + ); + + await expect( + svc.deliverOpenCodeMemberMessage(teamName, { + memberName: 'alice', + text: 'trigger a permission-blocked delivery without session evidence', + messageId: 'msg-pure-opencode-permission-blocked-no-session', + }) + ).resolves.toMatchObject({ + delivered: false, + responseState: 'permission_blocked', + }); + + const approvals = approvalEvents.filter( + (event): event is ToolApprovalRequest => + !('dismissed' in event) && !('autoResolved' in event) + ); + expect(approvals.map((event) => event.runtimePermission?.providerRequestId)).toEqual([ + 'perm-alice-delivery', + ]); + expect(approvals[0]).toMatchObject({ + requestId: `opencode:${launch.runId}:perm-alice-delivery`, + source: 'alice', + runtimePermission: { + providerId: 'opencode', + laneId: 'primary', + memberName: 'alice', + providerRequestId: 'perm-alice-delivery', + sessionId: 'session-alice', + }, + }); + }); + + it('uses current OpenCode runtime session evidence when persisted launch state is unavailable', async () => { + const teamName = 'pure-opencode-primary-permission-runtime-session-map-safe-e2e'; + const adapter = new PermissionBlockedWithoutSessionOpenCodeRuntimeAdapter(); + adapter.setRuntimePermissions('primary', [ + { + providerId: 'opencode', + requestId: 'perm-alice-delivery', + sessionId: 'session-alice', + tool: 'bash', + title: 'Run git status', + kind: 'tool', + raw: { patterns: ['git status'] }, + }, + { + providerId: 'opencode', + requestId: 'perm-unknown-session', + sessionId: 'session-charlie', + tool: 'bash', + title: 'Run npm test', + kind: 'tool', + raw: { patterns: ['npm test'] }, + }, + ]); + const svc = new TeamProvisioningService(); + svc.setRuntimeAdapterRegistry(new TeamRuntimeAdapterRegistry([adapter])); + const approvalEvents: ToolApprovalEvent[] = []; + svc.setToolApprovalEventEmitter((event) => approvalEvents.push(event)); + + const launch = await svc.createTeam( + { + teamName, + cwd: projectPath, + providerId: 'opencode', + model: 'opencode/big-pickle', + skipPermissions: true, + members: [ + { name: 'alice', role: 'Developer', providerId: 'opencode' }, + { name: 'bob', role: 'Reviewer', providerId: 'opencode' }, + ], + }, + () => undefined + ); + await fs.rm(path.join(getTeamsBasePath(), teamName, 'launch-state.json'), { force: true }); + + await expect( + svc.deliverOpenCodeMemberMessage(teamName, { + memberName: 'alice', + text: 'trigger permission-blocked delivery after launch-state disappeared', + messageId: 'msg-pure-opencode-permission-runtime-session-map', + }) + ).resolves.toMatchObject({ + delivered: false, + responseState: 'permission_blocked', + }); + + expect(adapter.permissionListInputs).toEqual([ + { + teamName, + laneId: 'primary', + cwd: projectPath, + memberName: 'alice', + sessionId: undefined, + }, + ]); + const approvals = approvalEvents.filter( + (event): event is ToolApprovalRequest => + !('dismissed' in event) && !('autoResolved' in event) + ); + expect(approvals.map((event) => event.runtimePermission?.providerRequestId)).toEqual([ + 'perm-alice-delivery', + ]); + expect(approvals[0]).toMatchObject({ + requestId: `opencode:${launch.runId}:perm-alice-delivery`, + source: 'alice', + runtimePermission: { + providerId: 'opencode', + laneId: 'primary', + memberName: 'alice', + providerRequestId: 'perm-alice-delivery', + sessionId: 'session-alice', + }, + }); + }); + it('refreshes stale OpenCode session evidence before direct delivery when MCP transport changed', async () => { const teamName = 'pure-opencode-direct-message-transport-refresh-safe-e2e'; const adapter = new FakeOpenCodeRuntimeAdapter(); @@ -18403,8 +19173,16 @@ class FakeOpenCodeRuntimeAdapter implements TeamLaunchRuntimeAdapter { readonly launchInputs: TeamRuntimeLaunchInput[] = []; readonly messageInputs: OpenCodeTeamRuntimeMessageInput[] = []; readonly permissionAnswerInputs: TeamRuntimePermissionAnswerInput[] = []; + readonly permissionListInputs: Array<{ + teamName: string; + laneId: string; + cwd: string; + memberName?: string; + sessionId?: string | null; + }> = []; readonly reconcileInputs: TeamRuntimeReconcileInput[] = []; readonly stopInputs: TeamRuntimeStopInput[] = []; + private readonly runtimePermissionsByLane = new Map(); constructor( private launchState: TeamRuntimeLaunchResult['teamLaunchState'] = 'clean_success', @@ -18419,6 +19197,10 @@ class FakeOpenCodeRuntimeAdapter implements TeamLaunchRuntimeAdapter { this.memberOutcomes = memberOutcomes; } + setRuntimePermissions(laneId: string, permissions: TeamRuntimePendingPermission[]): void { + this.runtimePermissionsByLane.set(laneId, permissions); + } + async prepare(input: TeamRuntimeLaunchInput): Promise { return { ok: true, @@ -18493,6 +19275,24 @@ class FakeOpenCodeRuntimeAdapter implements TeamLaunchRuntimeAdapter { }; } + async listRuntimePermissions( + input: TeamRuntimePermissionListInput + ): Promise { + const laneId = input.laneId ?? 'primary'; + const cwd = input.cwd ?? ''; + this.permissionListInputs.push({ + teamName: input.teamName, + laneId, + cwd, + memberName: input.memberName, + sessionId: input.sessionId, + }); + return { + permissions: [...(this.runtimePermissionsByLane.get(laneId) ?? [])], + diagnostics: [], + }; + } + async reconcile(input: TeamRuntimeReconcileInput): Promise { this.reconcileInputs.push(input); const members = Object.fromEntries( @@ -18674,6 +19474,86 @@ class VisibleReplyOpenCodeRuntimeAdapter extends FakeOpenCodeRuntimeAdapter { } } +class PermissionBlockedOpenCodeRuntimeAdapter extends FakeOpenCodeRuntimeAdapter { + override async sendMessageToMember( + input: OpenCodeTeamRuntimeMessageInput + ): Promise { + this.messageInputs.push(input); + return { + ok: false, + providerId: 'opencode', + memberName: input.memberName, + sessionId: `session-${input.memberName}`, + responseObservation: { + state: 'permission_blocked', + deliveredUserMessageId: null, + assistantMessageId: null, + toolCallNames: [], + visibleMessageToolCallId: null, + visibleReplyMessageId: null, + visibleReplyCorrelation: null, + latestAssistantPreview: null, + reason: 'OpenCode session has 1 pending permission request(s)', + }, + diagnostics: [ + 'OpenCode API error', + 'OpenCode session has 1 pending permission request(s)', + ], + }; + } +} + +class PermissionBlockedWithoutSessionOpenCodeRuntimeAdapter extends PermissionBlockedOpenCodeRuntimeAdapter { + override async sendMessageToMember( + input: OpenCodeTeamRuntimeMessageInput + ): Promise { + const result = await super.sendMessageToMember(input); + return { + ...result, + sessionId: undefined, + }; + } +} + +class PermissionBlockedInlineObserveOpenCodeRuntimeAdapter extends FakeOpenCodeRuntimeAdapter { + readonly observeInputs: Array< + OpenCodeTeamRuntimeMessageInput & { prePromptCursor?: string | null } + > = []; + + override async sendMessageToMember( + input: OpenCodeTeamRuntimeMessageInput + ): Promise { + this.messageInputs.push(input); + return { + ok: true, + providerId: 'opencode', + memberName: input.memberName, + sessionId: `session-${input.memberName}`, + runtimePromptMessageId: `prompt-${input.messageId ?? input.memberName}`, + prePromptCursor: 'cursor-before-inline-observe-permission', + responseObservation: { + state: 'tool_error', + deliveredUserMessageId: `delivered-${input.messageId ?? input.memberName}`, + assistantMessageId: `assistant-${input.messageId ?? input.memberName}`, + toolCallNames: ['agent-teams_message_send'], + visibleMessageToolCallId: `call-${input.messageId ?? input.memberName}`, + visibleReplyMessageId: null, + visibleReplyCorrelation: null, + latestAssistantPreview: null, + reason: 'message_send_tool_error_without_visible_reply_proof', + }, + diagnostics: ['OpenCode tool failed without output'], + }; + } + + async observeMessageDelivery( + input: OpenCodeTeamRuntimeMessageInput & { prePromptCursor?: string | null } + ): Promise { + this.observeInputs.push(input); + throw new Error('OpenCode session has 1 pending permission request(s)'); + } +} + class BootstrapCheckingOpenCodeRuntimeAdapter extends FakeOpenCodeRuntimeAdapter { readonly bootstrapCheckins: { memberName: string; runId: string; state: string }[] = []; diff --git a/test/main/services/team/TeamProvisioningLaunchFailurePolicy.test.ts b/test/main/services/team/TeamProvisioningLaunchFailurePolicy.test.ts index a98a03f8..9d0d6f38 100644 --- a/test/main/services/team/TeamProvisioningLaunchFailurePolicy.test.ts +++ b/test/main/services/team/TeamProvisioningLaunchFailurePolicy.test.ts @@ -89,6 +89,11 @@ describe('TeamProvisioningLaunchFailurePolicy', () => { 'Teammate did not join within the launch grace window.; process table unavailable' ) ).toBe(true); + expect( + isAutoClearableLaunchFailureReason( + 'CLI process exited (code 1) — team provisioned but not alive' + ) + ).toBe(true); expect(isAutoClearableLaunchFailureReason('model not found')).toBe(false); expect(isAutoClearableLaunchFailureReason(undefined)).toBe(false); }); diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index 35bb481a..26ab1a4d 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -26677,6 +26677,90 @@ describe('TeamProvisioningService', () => { }); }); + it('reconciles confirmed primary bootstrap after CLI provisioned-but-not-alive exit', async () => { + const teamName = 'primary-bootstrap-cli-provisioned-not-alive-heals'; + const bootstrapRunId = 'run-primary-cli-exit-after-bootstrap'; + const reason = 'CLI process exited (code 1) \u2014 team provisioned but not alive'; + writeTeamMeta(teamName, { + providerId: 'anthropic', + model: 'sonnet', + }); + writeMembersMeta(teamName, [{ name: 'tom', providerId: 'anthropic', model: 'sonnet' }]); + writeLaunchConfig(teamName, '/Users/test/proj', 'lead-session', ['tom']); + writeMemberBootstrapRunId(teamName, 'tom', bootstrapRunId); + writeBootstrapState( + teamName, + [ + { + name: 'tom', + status: 'bootstrap_confirmed', + lastAttemptAt: Date.parse('2026-05-25T20:13:46.326Z'), + lastObservedAt: Date.parse('2026-05-25T20:13:56.110Z'), + }, + ], + '2026-05-25T20:14:03.317Z', + { runId: bootstrapRunId } + ); + fs.writeFileSync( + getTeamLaunchStatePath(teamName), + `${JSON.stringify( + createPersistedLaunchSnapshot({ + teamName, + leadSessionId: 'lead-session', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + model: 'sonnet', + laneId: 'primary', + laneKind: 'primary', + laneOwnerProviderId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + runtimePid: 27_036, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: reason, + livenessKind: 'confirmed_bootstrap', + pidSource: 'persisted_metadata', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + runtimeLastSeenAt: '2026-05-25T20:13:46.326Z', + lastEvaluatedAt: '2026-05-25T20:14:05.411Z', + }, + }, + updatedAt: '2026-05-25T20:14:05.411Z', + }), + null, + 2 + )}\n`, + 'utf8' + ); + + const svc = new TeamProvisioningService(); + const result = await svc.getMemberSpawnStatuses(teamName); + + expect(result.teamLaunchState).toBe('clean_success'); + expect(result.statuses.tom).toMatchObject({ + status: 'online', + launchState: 'confirmed_alive', + bootstrapConfirmed: true, + runtimeAlive: false, + livenessKind: 'confirmed_bootstrap', + hardFailure: false, + error: undefined, + }); + expect(result.statuses.tom?.hardFailureReason).toBeUndefined(); + expect(result.statuses.tom?.runtimeDiagnostic).toBeUndefined(); + expect(result.statuses.tom?.runtimeDiagnosticSeverity).toBeUndefined(); + }); + it('cleans stale confirmed primary diagnostics from an already successful mixed launch', async () => { const teamName = 'mixed-confirmed-primary-stale-diagnostic-cleans'; writeTeamMeta(teamName, { From 353492eb18e63b77d02af2d9e2222d245a1cebc3 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 19:46:41 +0300 Subject: [PATCH 23/59] docs: update onboarding and product docs --- .github/CONTRIBUTING.md | 4 +++- README.md | 14 +++++++++++++- docs/iterations/edit-project/plan-architecture.md | 2 +- landing/product-docs/guide/installation.md | 4 +++- landing/product-docs/guide/quickstart.md | 2 ++ landing/product-docs/ru/guide/installation.md | 4 +++- landing/product-docs/ru/guide/quickstart.md | 2 +- 7 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ce84b163..d6e5dc07 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -9,10 +9,12 @@ For big features and major changes, please discuss them in our [Discord](https:/ Small fixes, bug reports, and minor improvements are always welcome - just open a PR. ## Prerequisites -- Node.js 20+ +- Node.js 24.16.0 LTS - pnpm 10+ - macOS, Windows, or Linux +On macOS, official Node.js 24 prebuilt binaries require macOS 13.5+ for source development. + ## Setup ```bash pnpm install diff --git a/README.md b/README.md index 5066777e..5b53cbc0 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,8 @@ An orchestration layer for AI agent teams across Claude, Codex, and OpenCode. - **Zero-setup onboarding** — start with the free model with no auth, then connect paid/account providers only when you need them +- **Multi-language support** - choose the app language and preferred agent communication language. Current UI languages: Arabic, Bengali, Chinese, English, French, German, Hindi, Indonesian, Japanese, Korean, Portuguese, Russian, Spanish, Urdu. + - **Built-in code editor** — edit project files with Git support without leaving the app - **Branch strategy** - choose per teammate at launch: use the main checkout or run selected agents in their own git worktree. You can still spell out branch rules in the provisioning prompt. @@ -283,7 +285,9 @@ Electron 40, React 19, TypeScript 5, Tailwind CSS 3, Zustand 4. Data from `~/.cl
-**Prerequisites:** Node.js 20+, pnpm 10+ +**Prerequisites:** Node.js 24.16.0 LTS, pnpm 10+ + +On macOS, official Node.js 24 prebuilt binaries require macOS 13.5+. ```bash git clone https://github.com/777genius/agent-teams-ai.git @@ -374,10 +378,18 @@ local packaging. See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for development guidelines. Please read our [Code of Conduct](.github/CODE_OF_CONDUCT.md). +## Partnerships + +We are open to partnerships and collaboration opportunities. If you see a way to create value together, we are ready to discuss mutually beneficial terms. + +Contact: [quantjumppro@gmail.com](mailto:quantjumppro@gmail.com) + ## Security IPC and standalone HTTP handlers validate IDs, paths, and payload shape at the boundary. Project editing and write operations are constrained to the selected project root, while read-only discovery also accesses local Claude data under `~/.claude/` and app-owned state paths when required. Path traversal and sensitive config/credential targets are blocked. See [SECURITY.md](.github/SECURITY.md) for details. +GitHub Dependabot monitors dependencies for known vulnerabilities, so security updates are surfaced quickly and applied in time. + ## License [AGPL-3.0](LICENSE) diff --git a/docs/iterations/edit-project/plan-architecture.md b/docs/iterations/edit-project/plan-architecture.md index 33f8f4eb..c32bb0e6 100644 --- a/docs/iterations/edit-project/plan-architecture.md +++ b/docs/iterations/edit-project/plan-architecture.md @@ -1266,7 +1266,7 @@ Binary: null bytes в первых 8KB или расширение (.png, .wasm) ### 19.8 File Watcher (Impact: MEDIUM) -Проект использует `fs.watch({ recursive: true })`, не chokidar. Electron 40/Node 20+ OK. +Проект использует `fs.watch({ recursive: true })`, не chokidar. Electron 40/Node 24.16+ OK. **Решение:** fs.watch + фильтр (node_modules/.git/dist) + debounce 200ms + **opt-in** (ручной F5 по умолчанию) + cleanup. diff --git a/landing/product-docs/guide/installation.md b/landing/product-docs/guide/installation.md index 80b7c747..44230025 100644 --- a/landing/product-docs/guide/installation.md +++ b/landing/product-docs/guide/installation.md @@ -49,9 +49,11 @@ For source development, you also need: | Tool | Version | | ------- | ------- | -| Node.js | 20+ | +| Node.js | 24.16.0 LTS | | pnpm | 10+ | +On macOS, official Node.js 24 prebuilt binaries require macOS 13.5+. + ## Run from source diff --git a/landing/product-docs/guide/quickstart.md b/landing/product-docs/guide/quickstart.md index 90d09ba8..744f70bd 100644 --- a/landing/product-docs/guide/quickstart.md +++ b/landing/product-docs/guide/quickstart.md @@ -51,6 +51,8 @@ For project conventions and architecture guidance, refer to these canonical file **Or run from source** for development: +Requires Node.js 24.16.0 LTS and pnpm 10+. On macOS, official Node.js 24 prebuilt binaries require macOS 13.5+. + ```bash git clone https://github.com/777genius/agent-teams-ai.git cd agent-teams-ai diff --git a/landing/product-docs/ru/guide/installation.md b/landing/product-docs/ru/guide/installation.md index 60eec9b5..a36bdb70 100644 --- a/landing/product-docs/ru/guide/installation.md +++ b/landing/product-docs/ru/guide/installation.md @@ -42,9 +42,11 @@ Gemini — поддерживаемый провайдер. Варианты aut | Инструмент | Версия | | ---------- | ------ | -| Node.js | 20+ | +| Node.js | 24.16.0 LTS | | pnpm | 10+ | +На macOS официальные prebuilt-бинарники Node.js 24 требуют macOS 13.5+. + ## Запуск из исходников diff --git a/landing/product-docs/ru/guide/quickstart.md b/landing/product-docs/ru/guide/quickstart.md index 60d5f9ea..b7cffe38 100644 --- a/landing/product-docs/ru/guide/quickstart.md +++ b/landing/product-docs/ru/guide/quickstart.md @@ -15,7 +15,7 @@ lang: ru-RU - **macOS, Windows или Linux** машина - **Git-репозиторий** в качестве проекта (рекомендуется для diff review и worktree isolation) - Бесплатная модель без авторизации для первого запуска или доступ к провайдеру, если нужны дополнительные модели: Anthropic (Claude), OpenAI (Codex), OpenRouter (OpenCode) или Google (Gemini) -- Node.js 20+ и pnpm 10+ при запуске из исходников +- Node.js 24.16.0 LTS и pnpm 10+ при запуске из исходников Подробности и ссылки для скачивания — в разделе [Установка](/ru/guide/installation). From 265d71e8b43c3ec82e1b130e52c521763b0f20bc Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 26 May 2026 23:07:08 +0300 Subject: [PATCH 24/59] ci: switch dev reviewrouter to codex rotating oauth --- .github/workflows/reviewrouter-codex.yml | 26 ++++++ .../workflows/reviewrouter-interaction.yml | 81 +++++++++++++++++-- .github/workflows/reviewrouter.yml | 51 ------------ 3 files changed, 99 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/reviewrouter-codex.yml delete mode 100644 .github/workflows/reviewrouter.yml diff --git a/.github/workflows/reviewrouter-codex.yml b/.github/workflows/reviewrouter-codex.yml new file mode 100644 index 00000000..29dbddc0 --- /dev/null +++ b/.github/workflows/reviewrouter-codex.yml @@ -0,0 +1,26 @@ +name: ReviewRouter Codex OAuth + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: {} + +jobs: + codex-review: + name: codex-review + runs-on: ubuntu-24.04 + timeout-minutes: 30 + if: ${{ github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.type != 'Bot' }} + permissions: + id-token: write + steps: + - name: ReviewRouter Codex OAuth review + id: run_codex + uses: 777genius/review-router@a8569b01f257b3af3e575450554fca63fb4b6c6d + with: + mode: codex-oauth-rotating + api-url: "https://api.reviewrouter.site" + provider-instance-id: "codex-rotating:1163183284" + workflow-schema-version: "1" + auth-json: ${{ secrets.REVIEWROUTER_CODEX_AUTH_JSON }} diff --git a/.github/workflows/reviewrouter-interaction.yml b/.github/workflows/reviewrouter-interaction.yml index 15ba9b9b..df7d385d 100644 --- a/.github/workflows/reviewrouter-interaction.yml +++ b/.github/workflows/reviewrouter-interaction.yml @@ -3,6 +3,8 @@ name: ReviewRouter Interaction on: pull_request_review_comment: types: [created, edited] + issue_comment: + types: [created, edited] workflow_dispatch: permissions: @@ -15,11 +17,74 @@ permissions: jobs: interaction: name: interaction - uses: 777genius/review-router/.github/workflows/reviewrouter-interaction-reusable.yml@v1 - with: - runtime_ref: v1 - api_url: "https://api.reviewrouter.site" - runtime_config_mode: oidc - review_workflow_file: reviewrouter.yml - secrets: - REVIEW_ROUTER_LEDGER_KEY: ${{ secrets.REVIEW_ROUTER_LEDGER_KEY }} + runs-on: ubuntu-24.04 + if: ${{ github.event_name == 'workflow_dispatch' || ((github.event_name != 'issue_comment' || github.event.issue.pull_request) && github.event.comment.user.type != 'Bot') }} + env: + REVIEWROUTER_API_URL: "https://api.reviewrouter.site" + REVIEWROUTER_ACTION_VERSION: "a8569b01f257b3af3e575450554fca63fb4b6c6d" + REVIEWROUTER_OIDC_AUDIENCE: "reviewrouter" + REVIEWROUTER_RUNTIME_CONFIG_MODE: "oidc" + REVIEWROUTER_STATIC_CONFIG_FALLBACK: "true" + REVIEWROUTER_COMMENT_TOKEN_MODE: "app-oidc" + CODEX_AUTH_JSON_PRESENT: ${{ secrets.REVIEWROUTER_CODEX_AUTH_JSON != '' && '1' || '0' }} + REVIEW_ROUTER_REVIEW_WORKFLOW_FILE: "reviewrouter-codex.yml" + steps: + - name: Fetch ReviewRouter runtime config + if: ${{ github.event_name != 'merge_group' && (github.event_name == 'workflow_dispatch' || github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + shell: bash + run: | + set -euo pipefail + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + echo "ReviewRouter OIDC is unavailable. Check id-token: write permission." + exit 1 + fi + echo "ReviewRouter runtime config will be fetched by the action using GitHub OIDC." + + - name: Preflight ReviewRouter interaction + id: preflight + uses: 777genius/review-router@a8569b01f257b3af3e575450554fca63fb4b6c6d + env: + GITHUB_TOKEN: ${{ github.token }} + REVIEW_ROUTER_MODE: "interaction-preflight" + REVIEW_ROUTER_DISCUSSION_MODE: ${{ vars.REVIEW_ROUTER_DISCUSSION_MODE || 'off' }} + + - name: Setup Node.js for Codex discussion replies + if: ${{ steps.preflight.outputs.needs_discussion == 'true' && env.CODEX_AUTH_JSON_PRESENT == '1' }} + uses: actions/setup-node@v6 + with: + node-version: "24" + + - name: Install Codex CLI for discussion replies + if: ${{ steps.preflight.outputs.needs_discussion == 'true' && env.CODEX_AUTH_JSON_PRESENT == '1' }} + shell: bash + run: npm install -g @openai/codex@0.125.0 + + - name: Restore Codex subscription auth for discussion replies + if: ${{ steps.preflight.outputs.needs_discussion == 'true' && env.CODEX_AUTH_JSON_PRESENT == '1' }} + shell: bash + env: + CODEX_AUTH_JSON: ${{ secrets.REVIEWROUTER_CODEX_AUTH_JSON }} + run: | + set -euo pipefail + if [ -z "${CODEX_AUTH_JSON:-}" ]; then + echo "::error::REVIEWROUTER_CODEX_AUTH_JSON secret is missing. Re-run ReviewRouter Codex setup." + exit 1 + fi + export CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" + mkdir -p "$CODEX_HOME" + chmod 700 "$CODEX_HOME" + printf '%s' "$CODEX_AUTH_JSON" > "$CODEX_HOME/auth.json" + chmod 600 "$CODEX_HOME/auth.json" + + - name: Run ReviewRouter interaction + if: ${{ steps.preflight.outputs.should_run == 'true' }} + uses: 777genius/review-router@a8569b01f257b3af3e575450554fca63fb4b6c6d + env: + GITHUB_TOKEN: ${{ github.token }} + REVIEW_ROUTER_MODE: "interaction" + REVIEW_ROUTER_DISCUSSION_MODE: ${{ vars.REVIEW_ROUTER_DISCUSSION_MODE || 'off' }} + REVIEW_ROUTER_DISCUSSION_MAX_PER_PR: ${{ vars.REVIEW_ROUTER_DISCUSSION_MAX_PER_PR || '20' }} + REVIEW_ROUTER_DISCUSSION_MAX_PER_THREAD: ${{ vars.REVIEW_ROUTER_DISCUSSION_MAX_PER_THREAD || '5' }} + REVIEW_ROUTER_DISCUSSION_TIMEOUT_SECONDS: ${{ vars.REVIEW_ROUTER_DISCUSSION_TIMEOUT_SECONDS || '60' }} + CODEX_MODEL: ${{ vars.REVIEW_CODEX_MODEL || 'gpt-5.5' }} + CODEX_REASONING_EFFORT: ${{ vars.REVIEW_CODEX_EFFORT || 'medium' }} diff --git a/.github/workflows/reviewrouter.yml b/.github/workflows/reviewrouter.yml deleted file mode 100644 index 116f1a27..00000000 --- a/.github/workflows/reviewrouter.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: ReviewRouter - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - merge_group: - workflow_dispatch: - inputs: - pr_number: - description: "Pull request number for manual reruns" - required: false - type: string - -permissions: - contents: read - pull-requests: write - issues: write - id-token: write - -jobs: - review: - name: review - uses: 777genius/review-router/.github/workflows/reviewrouter-reusable.yml@v1 - with: - runtime_ref: v1 - api_url: "https://api.reviewrouter.site" - runtime_config_mode: oidc - static_runtime_env_json: |- - { - "REVIEWROUTER_CONFIG_SCHEMA_VERSION": "2", - "CODEX_MODEL": "gpt-5.5", - "CODEX_REASONING_EFFORT": "medium", - "CODEX_AGENTIC_CONTEXT": "true", - "CODEX_FAST_MODE": "false", - "INLINE_MAX_COMMENTS": "5", - "INLINE_MIN_AGREEMENT": "1", - "TARGET_TOKENS_PER_BATCH": "50000", - "FAIL_ON_SEVERITY": "critical", - "PROVIDER_LIMIT": "1", - "PROVIDER_MAX_PARALLEL": "1", - "REVIEW_AUTH_MODE": "codex-oauth", - "REVIEW_PROVIDERS": "codex/gpt-5.5", - "SYNTHESIS_MODEL": "codex/gpt-5.5" - } - pr_number: ${{ github.event.pull_request.number || inputs.pr_number }} - secrets: - REVIEW_ROUTER_LEDGER_KEY: ${{ secrets.REVIEW_ROUTER_LEDGER_KEY }} - CODEX_AUTH_JSON: ${{ secrets.CODEX_AUTH_JSON }} - CODEX_CONFIG_TOML: ${{ secrets.CODEX_CONFIG_TOML }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} From a7ed38e1679077084a2fe27718472c1b7c164101 Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 26 May 2026 23:38:17 +0300 Subject: [PATCH 25/59] ci: bump reviewrouter codex action --- .github/workflows/reviewrouter-codex.yml | 2 +- .github/workflows/reviewrouter-interaction.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reviewrouter-codex.yml b/.github/workflows/reviewrouter-codex.yml index 29dbddc0..e614860d 100644 --- a/.github/workflows/reviewrouter-codex.yml +++ b/.github/workflows/reviewrouter-codex.yml @@ -17,7 +17,7 @@ jobs: steps: - name: ReviewRouter Codex OAuth review id: run_codex - uses: 777genius/review-router@a8569b01f257b3af3e575450554fca63fb4b6c6d + uses: 777genius/review-router@c23ae97660c3674a2ebc9076bb5cd4f1bbd85657 with: mode: codex-oauth-rotating api-url: "https://api.reviewrouter.site" diff --git a/.github/workflows/reviewrouter-interaction.yml b/.github/workflows/reviewrouter-interaction.yml index df7d385d..a018d65c 100644 --- a/.github/workflows/reviewrouter-interaction.yml +++ b/.github/workflows/reviewrouter-interaction.yml @@ -21,7 +21,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' || ((github.event_name != 'issue_comment' || github.event.issue.pull_request) && github.event.comment.user.type != 'Bot') }} env: REVIEWROUTER_API_URL: "https://api.reviewrouter.site" - REVIEWROUTER_ACTION_VERSION: "a8569b01f257b3af3e575450554fca63fb4b6c6d" + REVIEWROUTER_ACTION_VERSION: "c23ae97660c3674a2ebc9076bb5cd4f1bbd85657" REVIEWROUTER_OIDC_AUDIENCE: "reviewrouter" REVIEWROUTER_RUNTIME_CONFIG_MODE: "oidc" REVIEWROUTER_STATIC_CONFIG_FALLBACK: "true" @@ -42,7 +42,7 @@ jobs: - name: Preflight ReviewRouter interaction id: preflight - uses: 777genius/review-router@a8569b01f257b3af3e575450554fca63fb4b6c6d + uses: 777genius/review-router@c23ae97660c3674a2ebc9076bb5cd4f1bbd85657 env: GITHUB_TOKEN: ${{ github.token }} REVIEW_ROUTER_MODE: "interaction-preflight" @@ -78,7 +78,7 @@ jobs: - name: Run ReviewRouter interaction if: ${{ steps.preflight.outputs.should_run == 'true' }} - uses: 777genius/review-router@a8569b01f257b3af3e575450554fca63fb4b6c6d + uses: 777genius/review-router@c23ae97660c3674a2ebc9076bb5cd4f1bbd85657 env: GITHUB_TOKEN: ${{ github.token }} REVIEW_ROUTER_MODE: "interaction" From 1cae11da344121009e438eac4446e2433b4ea9c7 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 23:41:46 +0300 Subject: [PATCH 26/59] refactor(agent-attachments): use agent image mime types directly --- .../agent-attachments/core/domain/capabilities.ts | 4 ++-- src/features/agent-attachments/core/domain/types.ts | 3 +-- .../agent-attachments/core/domain/validation.ts | 11 +++++------ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/features/agent-attachments/core/domain/capabilities.ts b/src/features/agent-attachments/core/domain/capabilities.ts index d039587a..279e1560 100644 --- a/src/features/agent-attachments/core/domain/capabilities.ts +++ b/src/features/agent-attachments/core/domain/capabilities.ts @@ -1,7 +1,7 @@ import type { AgentAttachmentCapability, AgentAttachmentCapabilityTarget, - ProviderImageMimeType, + AgentImageMimeType, } from './types'; const DEFAULT_IMAGE_BYTES_PER_PROVIDER = 4 * 1024 * 1024; @@ -18,7 +18,7 @@ export const CLAUDE_IMAGE_MIME_TYPES = [ function supportedImagesOnly( displayText: string, - supportedImageMimeTypes: readonly ProviderImageMimeType[] = NATIVE_IMAGE_MIME_TYPES + supportedImageMimeTypes: readonly AgentImageMimeType[] = NATIVE_IMAGE_MIME_TYPES ): AgentAttachmentCapability { return { supportsImages: true, diff --git a/src/features/agent-attachments/core/domain/types.ts b/src/features/agent-attachments/core/domain/types.ts index cf30d3e6..1e0b3e09 100644 --- a/src/features/agent-attachments/core/domain/types.ts +++ b/src/features/agent-attachments/core/domain/types.ts @@ -3,7 +3,6 @@ export const AGENT_ATTACHMENT_SCHEMA_VERSION = 1 as const; export type AgentAttachmentKind = 'image' | 'file' | 'unsupported'; export type AgentImageMimeType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp'; -export type ProviderImageMimeType = AgentImageMimeType; export type ProviderFileMimeType = 'application/pdf' | 'text/*'; export type AttachmentDeliveryFailureCode = @@ -98,7 +97,7 @@ export interface AgentAttachmentCapabilityTarget { export interface AgentAttachmentCapability { supportsImages: boolean; supportsFiles: boolean; - supportedImageMimeTypes: ProviderImageMimeType[]; + supportedImageMimeTypes: AgentImageMimeType[]; supportedFileMimeTypes: ProviderFileMimeType[]; maxImages: number; maxFiles: number; diff --git a/src/features/agent-attachments/core/domain/validation.ts b/src/features/agent-attachments/core/domain/validation.ts index 74805ae3..6e63e6aa 100644 --- a/src/features/agent-attachments/core/domain/validation.ts +++ b/src/features/agent-attachments/core/domain/validation.ts @@ -7,7 +7,6 @@ import type { AgentImageMimeType, AttachmentValidationResult, ImageOptimizationBudget, - ProviderImageMimeType, } from './types'; const AGENT_IMAGE_MIME_TYPES = new Set([ @@ -23,7 +22,7 @@ const OPTIMIZABLE_AGENT_IMAGE_MIME_TYPES = new Set([ +const PROVIDER_IMAGE_MIME_TYPES = new Set([ 'image/png', 'image/jpeg', 'image/gif', @@ -34,8 +33,8 @@ export function isAgentImageMimeType(mimeType: string): mimeType is AgentImageMi return AGENT_IMAGE_MIME_TYPES.has(mimeType as AgentImageMimeType); } -export function isProviderImageMimeType(mimeType: string): mimeType is ProviderImageMimeType { - return PROVIDER_IMAGE_MIME_TYPES.has(mimeType as ProviderImageMimeType); +export function isProviderImageMimeType(mimeType: string): mimeType is AgentImageMimeType { + return PROVIDER_IMAGE_MIME_TYPES.has(mimeType as AgentImageMimeType); } function isOptimizableAgentImageMimeType( @@ -54,9 +53,9 @@ function isProviderFileMimeType(mimeType: string, supported: readonly string[]): function isCapabilityImageMimeType( mimeType: string, - supported: readonly ProviderImageMimeType[] + supported: readonly AgentImageMimeType[] ): boolean { - return supported.includes(mimeType as ProviderImageMimeType); + return supported.includes(mimeType as AgentImageMimeType); } export function classifyAttachmentMime(mimeType: string): AgentAttachmentKind { From b15de780cbdfdefb95429c3a8775d1d776728fac Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 23:41:54 +0300 Subject: [PATCH 27/59] fix(codex-account): keep account snapshots fresh --- .../composition/createCodexAccountFeature.ts | 24 ++-- .../renderer/hooks/useCodexAccountSnapshot.ts | 16 +++ .../mergeCodexProviderStatusWithSnapshot.ts | 19 ++- .../main/createCodexAccountFeature.test.ts | 115 +++++++++++++++++- ...rgeCodexProviderStatusWithSnapshot.test.ts | 60 +++++++++ .../renderer/useCodexAccountSnapshot.test.ts | 73 ++++++++++- .../useRuntimeProviderManagement.test.ts | 6 +- 7 files changed, 298 insertions(+), 15 deletions(-) diff --git a/src/features/codex-account/main/composition/createCodexAccountFeature.ts b/src/features/codex-account/main/composition/createCodexAccountFeature.ts index 5a9f39ec..80fd3c5e 100644 --- a/src/features/codex-account/main/composition/createCodexAccountFeature.ts +++ b/src/features/codex-account/main/composition/createCodexAccountFeature.ts @@ -263,7 +263,8 @@ async function resolveCodexBinaryForAccountSnapshot(): Promise { await resolveInteractiveShellEnvBestEffort({ timeoutMs: CODEX_BINARY_COLD_RETRY_TIMEOUT_MS, fallbackEnv: process.env, - background: false, + background: true, + source: 'codex-account-binary-discovery', }); CodexBinaryResolver.clearCache(); return CodexBinaryResolver.resolve(); @@ -293,6 +294,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade { private snapshotCache: CodexAccountSnapshotDto | null = null; private snapshotObservedAt = 0; + private lastPublishedSnapshotUpdatedAtMs = 0; private refreshPromise: Promise | null = null; private pendingRefreshOptions: CodexSnapshotRefreshOptions | null = null; private lastKnownAccount: CodexLastKnownAccount | null = null; @@ -446,6 +448,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade { this.lastKnownAccount = null; this.lastKnownRateLimits = null; this.lastKnownRuntimeContext = null; + this.lastPublishedSnapshotUpdatedAtMs = 0; this.activeMutationCount = 0; if (this.mutationQueueRelease) { this.mutationQueueRelease(); @@ -519,7 +522,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade { runtimeContext: freshRuntimeContext, login, rateLimits: this.snapshotCache?.rateLimits ?? null, - updatedAt: new Date(now).toISOString(), + updatedAt: new Date().toISOString(), }); return snapshot; } @@ -539,7 +542,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade { localActiveChatgptAccountPresent, login, rateLimits: null, - updatedAt: new Date(now).toISOString(), + updatedAt: new Date().toISOString(), }); return snapshot; } @@ -699,20 +702,27 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade { runtimeContext, login, rateLimits, - updatedAt: new Date(now).toISOString(), + updatedAt: new Date().toISOString(), }); return snapshot; } private setSnapshot(nextSnapshot: CodexAccountSnapshotDto): CodexAccountSnapshotDto { + const publishedAtMs = Math.max(Date.now(), this.lastPublishedSnapshotUpdatedAtMs + 1); + this.lastPublishedSnapshotUpdatedAtMs = publishedAtMs; + const publishedSnapshot = { + ...nextSnapshot, + updatedAt: new Date(publishedAtMs).toISOString(), + }; + if (this.disposed) { - return deepClone(nextSnapshot); + return deepClone(publishedSnapshot); } - this.snapshotCache = deepClone(nextSnapshot); + this.snapshotCache = deepClone(publishedSnapshot); this.snapshotObservedAt = Date.now(); - const snapshot = deepClone(nextSnapshot); + const snapshot = deepClone(publishedSnapshot); this.presenter.publish(snapshot); for (const listener of this.listeners) { listener(snapshot); diff --git a/src/features/codex-account/renderer/hooks/useCodexAccountSnapshot.ts b/src/features/codex-account/renderer/hooks/useCodexAccountSnapshot.ts index 1269ffd6..368d590f 100644 --- a/src/features/codex-account/renderer/hooks/useCodexAccountSnapshot.ts +++ b/src/features/codex-account/renderer/hooks/useCodexAccountSnapshot.ts @@ -42,6 +42,11 @@ function getRefreshIntervalMs(options: { : CODEX_VISIBLE_STANDARD_REFRESH_MS; } +function getSnapshotUpdatedAtMs(snapshot: CodexAccountSnapshotDto): number | null { + const updatedAtMs = Date.parse(snapshot.updatedAt); + return Number.isFinite(updatedAtMs) ? updatedAtMs : null; +} + export function useCodexAccountSnapshot(options: { enabled: boolean; includeRateLimits?: boolean; @@ -68,6 +73,7 @@ export function useCodexAccountSnapshot(options: { const [error, setError] = useState(null); const [visible, setVisible] = useState(() => isDocumentVisible()); const lastUpdatedAtRef = useRef(null); + const snapshotUpdatedAtRef = useRef(null); const initialRefreshDelayMs = options.initialRefreshDelayMs ?? 0; const initialRefreshMaxDelayMs = options.initialRefreshMaxDelayMs; const [initialRefreshAttempted, setInitialRefreshAttempted] = useState( @@ -75,6 +81,16 @@ export function useCodexAccountSnapshot(options: { ); const applySnapshot = useCallback((nextSnapshot: CodexAccountSnapshotDto) => { + const nextUpdatedAtMs = getSnapshotUpdatedAtMs(nextSnapshot); + if ( + nextUpdatedAtMs !== null && + snapshotUpdatedAtRef.current !== null && + nextUpdatedAtMs < snapshotUpdatedAtRef.current + ) { + return; + } + + snapshotUpdatedAtRef.current = nextUpdatedAtMs ?? Date.now(); lastUpdatedAtRef.current = Date.now(); setSnapshot(nextSnapshot); setError(null); diff --git a/src/features/codex-account/renderer/mergeCodexProviderStatusWithSnapshot.ts b/src/features/codex-account/renderer/mergeCodexProviderStatusWithSnapshot.ts index 8980cf69..620cd1f7 100644 --- a/src/features/codex-account/renderer/mergeCodexProviderStatusWithSnapshot.ts +++ b/src/features/codex-account/renderer/mergeCodexProviderStatusWithSnapshot.ts @@ -144,6 +144,21 @@ function mergeCodexNativeBackendOption( }); } +function mergeCodexCapabilitiesWithSnapshot( + provider: CliProviderStatus, + snapshot: CodexAccountSnapshotDto +): CliProviderStatus['capabilities'] { + if (!snapshot.launchAllowed) { + return provider.capabilities; + } + + return { + ...provider.capabilities, + teamLaunch: true, + oneShot: true, + }; +} + export function mergeCodexProviderStatusWithSnapshot( provider: CliProviderStatus, snapshot: CodexAccountSnapshotDto | null @@ -166,8 +181,10 @@ export function mergeCodexProviderStatusWithSnapshot( return { ...provider, - supported: provider.supported || isCodexBootstrapPlaceholder(provider), + supported: + provider.supported || isCodexBootstrapPlaceholder(provider) || snapshot.launchAllowed, authenticated: snapshot.launchAllowed, + capabilities: mergeCodexCapabilitiesWithSnapshot(provider, snapshot), authMethod: snapshot.effectiveAuthMode === 'chatgpt' ? 'chatgpt' diff --git a/test/features/codex-account/main/createCodexAccountFeature.test.ts b/test/features/codex-account/main/createCodexAccountFeature.test.ts index 623103c0..1b4e611f 100644 --- a/test/features/codex-account/main/createCodexAccountFeature.test.ts +++ b/test/features/codex-account/main/createCodexAccountFeature.test.ts @@ -370,9 +370,23 @@ describe('createCodexAccountFeature', () => { }); it('retries Codex binary discovery after cold shell env resolves before publishing runtime-missing', async () => { - binaryResolveMock.mockResolvedValueOnce(null).mockResolvedValue('/usr/local/bin/codex'); - resolveInteractiveShellEnvBestEffortMock.mockResolvedValue({ - PATH: '/usr/local/bin:/usr/bin:/bin', + getCachedShellEnvMock.mockReturnValue(null); + binaryResolveMock.mockImplementation(async () => + getCachedShellEnvMock()?.PATH?.includes('/custom/bin') ? '/custom/bin/codex' : null + ); + resolveInteractiveShellEnvBestEffortMock.mockImplementation(async (options?: { + background?: boolean; + fallbackEnv?: NodeJS.ProcessEnv; + }) => { + if (options?.background === false) { + return options.fallbackEnv ?? {}; + } + + const shellEnv = { + PATH: '/custom/bin:/usr/bin:/bin', + }; + getCachedShellEnvMock.mockReturnValue(shellEnv); + return shellEnv; }); readAccountMock.mockResolvedValue({ account: createAccountResponse(), @@ -395,6 +409,8 @@ describe('createCodexAccountFeature', () => { expect.objectContaining({ timeoutMs: 12_000, fallbackEnv: process.env, + background: true, + source: 'codex-account-binary-discovery', }) ); expect(binaryClearCacheMock).toHaveBeenCalledTimes(1); @@ -407,6 +423,99 @@ describe('createCodexAccountFeature', () => { } }); + it('timestamps snapshots at publication time after a slow account read', async () => { + vi.useFakeTimers({ + toFake: ['Date'], + }); + vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); + const accountReadDeferred = createDeferred(); + readAccountMock.mockImplementation(async () => { + await accountReadDeferred.promise; + return { + account: createAccountResponse(), + initialize: { + codexHome: '/Users/test/.codex', + platformFamily: 'unix', + platformOs: 'macos', + }, + }; + }); + + const feature = createCodexAccountFeature({ + logger: createLoggerPort(), + configManager: createConfigManager('chatgpt'), + }); + + try { + const snapshotPromise = feature.refreshSnapshot(); + await vi.waitFor(() => { + expect(readAccountMock).toHaveBeenCalledTimes(1); + }); + + vi.setSystemTime(new Date('2026-01-01T00:00:05.000Z')); + accountReadDeferred.resolve(); + + const snapshot = await snapshotPromise; + + expect(snapshot.updatedAt).toBe('2026-01-01T00:00:05.000Z'); + expect(snapshot.appServerState).toBe('healthy'); + } finally { + vi.useRealTimers(); + await feature.dispose(); + } + }); + + it('publishes strictly increasing snapshot timestamps within the same millisecond', async () => { + vi.useFakeTimers({ + toFake: ['Date'], + }); + vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); + readAccountMock.mockResolvedValue({ + account: createAccountResponse(), + initialize: { + codexHome: '/Users/test/.codex', + platformFamily: 'unix', + platformOs: 'macos', + }, + }); + + const feature = createCodexAccountFeature({ + logger: createLoggerPort(), + configManager: createConfigManager('chatgpt'), + }); + const publishedSnapshots: CodexAccountSnapshotDto[] = []; + const unsubscribe = feature.subscribe((snapshot) => { + publishedSnapshots.push(snapshot); + }); + + try { + await feature.refreshSnapshot(); + emitLoginState({ + status: 'pending', + error: null, + startedAt: '2026-01-01T00:00:00.000Z', + authUrl: 'https://chatgpt.com/auth', + }); + emitLoginState({ + status: 'cancelled', + error: null, + startedAt: null, + authUrl: null, + }); + + expect(publishedSnapshots.map((snapshot) => Date.parse(snapshot.updatedAt))).toEqual([ + 1_767_225_600_000, + 1_767_225_600_001, + 1_767_225_600_002, + ]); + expect(publishedSnapshots.at(-1)?.login.status).toBe('cancelled'); + } finally { + unsubscribe(); + vi.useRealTimers(); + await feature.dispose(); + } + }); + it('still reports runtime-missing after the cold binary retry cannot find Codex', async () => { binaryResolveMock.mockResolvedValue(null); resolveInteractiveShellEnvBestEffortMock.mockResolvedValue({ diff --git a/test/features/codex-account/renderer/mergeCodexProviderStatusWithSnapshot.test.ts b/test/features/codex-account/renderer/mergeCodexProviderStatusWithSnapshot.test.ts index 269fc3dc..ef3df4b3 100644 --- a/test/features/codex-account/renderer/mergeCodexProviderStatusWithSnapshot.test.ts +++ b/test/features/codex-account/renderer/mergeCodexProviderStatusWithSnapshot.test.ts @@ -150,6 +150,66 @@ describe('mergeCodexProviderStatusWithSnapshot', () => { }); }); + it('clears a stale runtime-missing provider once the live snapshot is ready', () => { + const baseProvider = createBaseCodexProvider(); + const baseConnection = baseProvider.connection!; + const merged = mergeCodexProviderStatusWithSnapshot( + { + ...baseProvider, + supported: false, + authenticated: false, + verificationState: 'error', + statusMessage: 'Codex CLI not found. Install Codex to use native account management.', + capabilities: { + teamLaunch: false, + oneShot: false, + extensions: createDefaultCliExtensionCapabilities(), + }, + availableBackends: [ + { + id: 'codex-native', + label: 'Codex native', + description: 'Use codex exec JSON mode.', + selectable: false, + recommended: true, + available: false, + state: 'runtime-missing', + audience: 'general', + statusMessage: 'Codex CLI not found', + detailMessage: 'Codex CLI not found', + }, + ], + connection: { + ...baseConnection, + codex: { + ...baseConnection.codex!, + appServerState: 'runtime-missing', + appServerStatusMessage: 'Codex CLI not found', + launchAllowed: false, + launchIssueMessage: 'Codex CLI not found', + launchReadinessState: 'runtime_missing', + }, + }, + }, + createReadyChatgptSnapshot() + ); + + expect(merged.supported).toBe(true); + expect(merged.authenticated).toBe(true); + expect(merged.verificationState).toBe('verified'); + expect(merged.statusMessage).toBe('ChatGPT account ready'); + expect(merged.capabilities.teamLaunch).toBe(true); + expect(merged.capabilities.oneShot).toBe(true); + expect(merged.connection?.codex?.appServerState).toBe('healthy'); + expect(merged.connection?.codex?.launchReadinessState).toBe('ready_chatgpt'); + expect(merged.availableBackends?.find((option) => option.id === 'codex-native')).toMatchObject({ + available: true, + selectable: true, + state: 'ready', + statusMessage: 'Ready', + }); + }); + it('hydrates codex connection truth even when the stale provider payload had no connection block', () => { const merged = mergeCodexProviderStatusWithSnapshot( { diff --git a/test/features/codex-account/renderer/useCodexAccountSnapshot.test.ts b/test/features/codex-account/renderer/useCodexAccountSnapshot.test.ts index 5c769e68..73d4ebf9 100644 --- a/test/features/codex-account/renderer/useCodexAccountSnapshot.test.ts +++ b/test/features/codex-account/renderer/useCodexAccountSnapshot.test.ts @@ -13,7 +13,9 @@ const apiMocks = vi.hoisted(() => ({ startCodexChatgptLogin: vi.fn(), cancelCodexChatgptLogin: vi.fn(), logoutCodexAccount: vi.fn(), - onCodexAccountSnapshotChanged: vi.fn(() => () => undefined), + onCodexAccountSnapshotChanged: vi.fn< + (callback: (event: unknown, snapshot: CodexAccountSnapshotDto) => void) => () => void + >(() => () => undefined), })); type IdleCallbackForTest = (deadline: { @@ -71,6 +73,16 @@ function createSnapshot(): CodexAccountSnapshotDto { }; } +function withSnapshotOverrides( + snapshot: CodexAccountSnapshotDto, + overrides: Partial +): CodexAccountSnapshotDto { + return { + ...snapshot, + ...overrides, + }; +} + function createDeferred() { let resolve!: (value: T | PromiseLike) => void; let reject!: (reason?: unknown) => void; @@ -133,6 +145,65 @@ describe('useCodexAccountSnapshot', () => { }); }); + it('ignores older pushed Codex snapshots after a fresher snapshot was applied', async () => { + let snapshotListener: + | ((event: unknown, snapshot: CodexAccountSnapshotDto) => void) + | null = null; + const staleSnapshot = withSnapshotOverrides(createSnapshot(), { + updatedAt: '2026-01-01T00:00:00.000Z', + managedAccount: { + type: 'chatgpt', + email: 'stale@example.com', + planType: 'pro', + }, + }); + const freshSnapshot = withSnapshotOverrides(createSnapshot(), { + updatedAt: '2026-01-01T00:00:01.000Z', + managedAccount: { + type: 'chatgpt', + email: 'fresh@example.com', + planType: 'pro', + }, + }); + apiMocks.getCodexAccountSnapshot.mockResolvedValue(freshSnapshot); + apiMocks.onCodexAccountSnapshotChanged.mockImplementation( + (callback: (event: unknown, snapshot: CodexAccountSnapshotDto) => void) => { + snapshotListener = callback; + return () => undefined; + } + ); + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + function Harness(): React.ReactElement { + const state = useCodexAccountSnapshot({ + enabled: true, + }); + + return React.createElement('div', null, state.snapshot?.managedAccount?.email ?? 'empty'); + } + + await act(async () => { + root.render(React.createElement(Harness)); + await Promise.resolve(); + }); + + expect(host.textContent).toContain('fresh@example.com'); + + await act(async () => { + snapshotListener?.({}, staleSnapshot); + await Promise.resolve(); + }); + + expect(host.textContent).toContain('fresh@example.com'); + + act(() => { + root.unmount(); + }); + }); + it('can defer the initial Codex snapshot without starting interval refreshes first', async () => { vi.useFakeTimers(); const snapshot = createSnapshot(); diff --git a/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts b/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts index 6c85685c..82c5f774 100644 --- a/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts +++ b/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts @@ -228,11 +228,11 @@ describe('useRuntimeProviderManagement', () => { const root = createRoot(host); await act(async () => { root.render(React.createElement(ConfigurableHarness, { enabled: true })); - await Promise.resolve(); - await Promise.resolve(); }); - expect(state?.error).toContain('wrong runtime binary'); + await vi.waitFor(() => { + expect(state?.error).toContain('wrong runtime binary'); + }); expect(state?.errorDiagnostics?.binaryPath).toBe('/opt/homebrew/bin/opencode'); await act(async () => { From c79b7d423481b3130eb735911603c64feb2729d3 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 23:42:01 +0300 Subject: [PATCH 28/59] fix(team): suppress unverified relay state claims --- .../services/team/TeamProvisioningService.ts | 39 +++- .../team/TeamProvisioningServiceRelay.test.ts | 198 ++++++++++++++++++ 2 files changed, 236 insertions(+), 1 deletion(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 4fe7464e..fb0b1c9f 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -3153,6 +3153,36 @@ function normalizeSameTeamText(text: string): string { return text.trim().replace(/\r\n/g, '\n'); } +function shouldSuppressUnverifiedLeadRelayStateLine(text: string): boolean { + const normalized = text.trim().replace(/\s+/g, ' '); + if (normalized.length === 0) { + return false; + } + + const hasStateSubject = + /#[a-z0-9]{4,}/i.test(normalized) || + /\bpr\s*#?\d+\b/i.test(normalized) || + /\bpull request\b/i.test(normalized) || + /\b(?:task|tasks|kanban|board|review|approval|merge|merged|branch|queue|worktree|commit|mergecommit|mergedat)\b/i.test( + normalized + ); + if (!hasStateSubject) { + return false; + } + + return ( + /\b(?:confirmed|verified|already|claims?|false|phantom|ground[- ]truth)\b/i.test(normalized) || + /\b(?:done|complete(?:d)?|approved|merged|closed|blocked|resolved|failed|succeeded)\b/i.test( + normalized + ) || + /\b(?:is|are|was|were|stays?|still|now)\s+(?:open|closed|merged|approved|complete(?:d)?|done|blocked|pending|in_progress|in progress|needsfix|needs fix|in review|clear)\b/i.test( + normalized + ) || + /\b(?:mergecommit|mergedat)\s*=\s*(?:null|[^\s,;]+)/i.test(normalized) || + /\bqueue\b.*\bclear\b/i.test(normalized) + ); +} + function getOpenCodeInboxRelayPriority( message: Pick ): number { @@ -6944,7 +6974,7 @@ export class TeamProvisioningService { return null; } const direct = candidates.find(([key]) => key === memberName); - const [key, member] = direct ?? candidates[0]!; + const [key, member] = direct ?? candidates[0]; return { key, member }; } @@ -23333,6 +23363,7 @@ export class TeamProvisioningService { `Plain text reply visibility for this batch: internal lead activity only.`, `Do NOT write a user-facing summary for teammate/system/cross-team relay traffic. If the human user must be notified, explicitly call SendMessage with recipient "user".`, `If you take action and no visible message/tool result already records it, you may write one terse internal status line for the team activity log.`, + `Do not use that internal status line to confirm, correct, or relay task, kanban, review, PR, branch, merge, or queue state unless you verified it with the source-of-truth tool in this turn.`, `If a visible reply is needed for a teammate, another team, or the human user, use the appropriate messaging tool instead of relying on plain text.`, ]; @@ -23349,6 +23380,7 @@ export class TeamProvisioningService { [ `Internal note: for task assignments, prefer task_create and rely on the board/runtime notification path instead of sending a separate SendMessage for the same assignment.`, `For any MCP board tool call in this turn, teamName MUST be "${teamName}". Never use the lead/member name "${leadName}" as teamName.`, + `Treat teammate/system/cross-team claims about task, kanban, review, PR, branch, merge, or queue state as unverified until checked. Before confirming, correcting, relaying, or acting on that state, call the relevant source-of-truth tool first (task_get/task_list/review/kanban tooling, or an available repository/GitHub command/tool). If you have not verified it in this turn, say verification is needed instead of stating the claim as fact.`, `A member_work_sync_status call alone is incomplete for Message kind: member_work_sync_nudge. Do not stop until member_work_sync_report succeeds or a real blocker is recorded.`, `Use task_create_from_message only for messages below that explicitly say "Eligible for task_create_from_message: yes" and provide a User MessageId. Never use task_create_from_message for teammate messages, system notifications, cross-team messages, or any inbox row that is not explicitly marked eligible.`, `If a message below is marked Source: system_notification and its summary looks like "Comment on #...", reply via task_add_comment only when you have a substantive board update (decision, blocker, clarification answer, review result, or concrete next-step change).`, @@ -23517,6 +23549,11 @@ export class TeamProvisioningService { (replyVisibility === 'user' && capturedUserVisibleSendMessage) ) { logger.debug(`[${teamName}] Suppressed lead relay text duplicated by visible message`); + } else if ( + replyVisibility === 'internal_activity' && + shouldSuppressUnverifiedLeadRelayStateLine(cleanReply) + ) { + logger.debug(`[${teamName}] Suppressed unverified lead relay state claim`); } else if (replyVisibility === 'internal_activity') { this.pushLiveLeadTextMessage( run, diff --git a/test/main/services/team/TeamProvisioningServiceRelay.test.ts b/test/main/services/team/TeamProvisioningServiceRelay.test.ts index fd6224f9..288bc228 100644 --- a/test/main/services/team/TeamProvisioningServiceRelay.test.ts +++ b/test/main/services/team/TeamProvisioningServiceRelay.test.ts @@ -499,6 +499,12 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => { message?: { content?: Array<{ text?: string }> }; }; const relayedPrompt = payload.message?.content?.[0]?.text ?? ''; + expect(relayedPrompt).toContain( + 'Do not use that internal status line to confirm, correct, or relay task, kanban, review, PR, branch, merge, or queue state unless you verified it with the source-of-truth tool in this turn.' + ); + expect(relayedPrompt).toContain( + 'Treat teammate/system/cross-team claims about task, kanban, review, PR, branch, merge, or queue state as unverified until checked.' + ); (service as any).handleStreamJsonMessage(run, { type: 'assistant', @@ -513,6 +519,111 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => { expect(hoisted.files.get(`/mock/teams/${teamName}/sentMessages.json`)).toBeUndefined(); }); + it('suppresses unverified non-user lead relay state claims from internal activity', async () => { + const service = new TeamProvisioningService(); + const teamName = 'my-team'; + seedConfig(teamName); + seedLeadInbox(teamName, [ + { + from: 'tom', + text: '#f8d7235a done.', + timestamp: '2026-02-23T10:00:00.000Z', + read: false, + summary: '#f8d7235a done', + messageId: 'm-1', + }, + ]); + + const { writeSpy } = attachAliveRun(service, teamName); + const relayPromise = service.relayLeadInboxMessages(teamName); + const run = await waitForCapture(service); + const payload = JSON.parse(String(writeSpy.mock.calls[0]?.[0] ?? '{}')) as { + message?: { content?: Array<{ text?: string }> }; + }; + const relayedPrompt = payload.message?.content?.[0]?.text ?? ''; + + (service as any).handleStreamJsonMessage(run, { + type: 'assistant', + content: [ + { + type: 'text', + text: + `Human: ${relayedPrompt}\n\n` + + 'Confirmed - both claims in msg 17eb3109 were false. #38730980 already approved and PR #38 is OPEN, mergeCommit=null.', + }, + ], + }); + (service as any).handleStreamJsonMessage(run, { type: 'result', subtype: 'success' }); + + await expect(relayPromise).resolves.toBe(1); + expect(service.getLiveLeadProcessMessages(teamName)).toHaveLength(0); + expect(hoisted.files.get(`/mock/teams/${teamName}/sentMessages.json`)).toBeUndefined(); + }); + + it.each([ + { + caseName: 'keeps task-ref delegation status', + replyText: 'Delegated #f8d7235a to bob.', + expectedLiveText: 'Delegated #f8d7235a to bob.', + }, + { + caseName: 'keeps verification-needed status', + replyText: 'Verification needed before confirming #f8d7235a.', + expectedLiveText: 'Verification needed before confirming #f8d7235a.', + }, + { + caseName: 'suppresses completed task claim', + replyText: 'Task #f8d7235a is completed.', + expectedLiveText: null, + }, + { + caseName: 'suppresses merged PR claim', + replyText: 'PR #38 merged.', + expectedLiveText: null, + }, + { + caseName: 'suppresses queue-clear claim', + replyText: 'Queue genuinely clear for #f8d7235a.', + expectedLiveText: null, + }, + ])( + 'classifies non-user lead relay internal activity: $caseName', + async ({ caseName, replyText, expectedLiveText }) => { + const service = new TeamProvisioningService(); + const teamName = `my-team-${caseName.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`; + seedConfig(teamName); + seedLeadInbox(teamName, [ + { + from: 'tom', + text: '#f8d7235a done.', + timestamp: '2026-02-23T10:00:00.000Z', + read: false, + summary: '#f8d7235a done', + messageId: 'm-1', + }, + ]); + + const { writeSpy } = attachAliveRun(service, teamName); + const relayPromise = service.relayLeadInboxMessages(teamName); + const run = await waitForCapture(service); + const payload = JSON.parse(String(writeSpy.mock.calls[0]?.[0] ?? '{}')) as { + message?: { content?: Array<{ text?: string }> }; + }; + const relayedPrompt = payload.message?.content?.[0]?.text ?? ''; + + (service as any).handleStreamJsonMessage(run, { + type: 'assistant', + content: [{ type: 'text', text: `Human: ${relayedPrompt}\n\n${replyText}` }], + }); + (service as any).handleStreamJsonMessage(run, { type: 'result', subtype: 'success' }); + + await expect(relayPromise).resolves.toBe(1); + const liveTexts = service.getLiveLeadProcessMessages(teamName).map((message) => message.text); + expect(liveTexts).toEqual(expectedLiveText === null ? [] : [expectedLiveText]); + expect(hoisted.files.get(`/mock/teams/${teamName}/sentMessages.json`)).toBeUndefined(); + } + ); + it('keeps user-originated lead relay replies user-visible', async () => { const service = new TeamProvisioningService(); const teamName = 'my-team'; @@ -552,6 +663,43 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => { expect(sentRows).toMatchObject([{ text: 'Creating the task now.', to: 'user' }]); }); + it('does not suppress state-like user-originated lead relay replies', async () => { + const service = new TeamProvisioningService(); + const teamName = 'my-team'; + seedConfig(teamName); + seedLeadInbox(teamName, [ + { + from: 'user', + text: 'What is the task status?', + timestamp: '2026-02-23T10:00:00.000Z', + read: false, + summary: 'Task status', + messageId: 'user-msg-state-like', + source: 'user_sent', + }, + ]); + + attachAliveRun(service, teamName); + const relayPromise = service.relayLeadInboxMessages(teamName); + const run = await waitForCapture(service); + + (service as any).handleStreamJsonMessage(run, { + type: 'assistant', + content: [{ type: 'text', text: 'Task #f8d7235a is completed.' }], + }); + (service as any).handleStreamJsonMessage(run, { type: 'result', subtype: 'success' }); + + await expect(relayPromise).resolves.toBe(1); + const live = service.getLiveLeadProcessMessages(teamName); + expect(live.map((message) => ({ to: message.to, text: message.text }))).toEqual([ + { to: 'user', text: 'Task #f8d7235a is completed.' }, + ]); + const sentRows = JSON.parse( + hoisted.files.get(`/mock/teams/${teamName}/sentMessages.json`) ?? '[]' + ) as Array<{ text?: string; to?: string }>; + expect(sentRows).toMatchObject([{ to: 'user', text: 'Task #f8d7235a is completed.' }]); + }); + it('does not mix internal lead relay rows into a user-visible relay batch', async () => { const service = new TeamProvisioningService(); const teamName = 'my-team'; @@ -708,6 +856,56 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => { ]); }); + it('keeps explicit teammate SendMessage from non-user lead relay visible', async () => { + const service = new TeamProvisioningService(); + const teamName = 'my-team'; + seedConfig(teamName); + seedLeadInbox(teamName, [ + { + from: 'bob', + text: 'Alice should review the release notes.', + timestamp: '2026-02-23T10:00:00.000Z', + read: false, + summary: 'Ask Alice', + messageId: 'internal-msg-teammate-send', + }, + ]); + + attachAliveRun(service, teamName); + const relayPromise = service.relayLeadInboxMessages(teamName); + const run = await waitForCapture(service); + + (service as any).handleStreamJsonMessage(run, { + type: 'assistant', + content: [ + { type: 'text', text: 'Sending Alice the handoff.' }, + { + type: 'tool_use', + name: 'SendMessage', + input: { + recipient: 'alice', + content: 'Please review the release notes.', + summary: 'Review release notes', + }, + }, + ], + }); + (service as any).handleStreamJsonMessage(run, { type: 'result', subtype: 'success' }); + + await expect(relayPromise).resolves.toBe(1); + const live = service.getLiveLeadProcessMessages(teamName); + expect(live.map((message) => ({ to: message.to, text: message.text }))).toEqual([ + { to: 'alice', text: 'Please review the release notes.' }, + ]); + const aliceInbox = JSON.parse( + hoisted.files.get(`/mock/teams/${teamName}/inboxes/alice.json`) ?? '[]' + ) as Array<{ member?: string; text?: string }>; + expect(aliceInbox).toMatchObject([ + { member: 'alice', text: 'Please review the release notes.' }, + ]); + expect(hoisted.files.get(`/mock/teams/${teamName}/sentMessages.json`)).toBeUndefined(); + }); + it('keeps user-originated plain reply when the lead also messages a teammate', async () => { const service = new TeamProvisioningService(); const teamName = 'my-team'; From ab6ab1fc4c8974b7bfd6644b9263dff4f3240b27 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 26 May 2026 23:42:06 +0300 Subject: [PATCH 29/59] test(team): cover provisioned runtime recovery --- .../team/TeamProvisioningService.test.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index 26ab1a4d..6e8c9204 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -4933,6 +4933,79 @@ describe('TeamProvisioningService', () => { expect(persisted.members.tom?.runtimeDiagnostic).not.toBe(staleDiagnostic); }); + it('exposes confirmed runtime snapshot after CLI provisioned-but-not-alive launch cleanup', async () => { + const teamName = 'zz-runtime-snapshot-cli-provisioned-not-alive-heals'; + const leadSessionId = 'lead-session'; + const projectPath = '/Users/test/proj'; + const bootstrapRunId = 'run-runtime-snapshot-cli-exit-after-bootstrap'; + const reason = 'CLI process exited (code 1) \u2014 team provisioned but not alive'; + writeLaunchConfig(teamName, projectPath, leadSessionId, ['tom']); + writeMemberBootstrapRunId(teamName, 'tom', bootstrapRunId); + writeLaunchState( + teamName, + leadSessionId, + { + tom: { + providerId: 'anthropic', + model: 'sonnet', + laneId: 'primary', + laneKind: 'primary', + laneOwnerProviderId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + runtimePid: 27_036, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: reason, + livenessKind: 'confirmed_bootstrap', + pidSource: 'persisted_metadata', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + runtimeLastSeenAt: '2026-05-25T20:13:46.326Z', + lastEvaluatedAt: '2026-05-25T20:14:05.411Z', + }, + }, + { launchPhase: 'finished', updatedAt: '2026-05-25T20:14:05.411Z' } + ); + writeBootstrapState( + teamName, + [ + { + name: 'tom', + status: 'bootstrap_confirmed', + lastAttemptAt: Date.parse('2026-05-25T20:13:46.326Z'), + lastObservedAt: Date.parse('2026-05-25T20:13:56.110Z'), + }, + ], + '2026-05-25T20:14:03.317Z', + { runId: bootstrapRunId } + ); + + const svc = new TeamProvisioningService(); + const snapshot = await svc.getTeamAgentRuntimeSnapshot(teamName); + const persisted = JSON.parse(fs.readFileSync(getTeamLaunchStatePath(teamName), 'utf8')); + + expect(snapshot.members.tom).toMatchObject({ + alive: true, + providerId: 'anthropic', + runtimeModel: 'sonnet', + livenessKind: 'confirmed_bootstrap', + historicalBootstrapConfirmed: true, + runtimeDiagnostic: 'bootstrap confirmed', + runtimeDiagnosticSeverity: 'info', + }); + expect(persisted.members.tom).toMatchObject({ + launchState: 'confirmed_alive', + bootstrapConfirmed: true, + hardFailure: false, + }); + expect(persisted.members.tom?.hardFailureReason).toBeUndefined(); + }); + it('does not treat a reused OpenCode runtime pid as live', async () => { const teamName = 'pure-opencode-reused-pid-team'; const projectPath = '/Users/test/project'; From 3849c019558c38d2e42105b7c9cac1d8955a1aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D0=B8=D1=8F?= Date: Tue, 26 May 2026 23:51:17 +0300 Subject: [PATCH 30/59] fix(provenance): classify synthetic user turns * fix(provenance): classify synthetic user turns * fix(provenance): keep assistant display rendering intact * fix(provenance): preserve source tool result rows --- .../memberLogPreviewExtractor.test.ts | 31 +++ .../policies/memberLogPreviewExtractor.ts | 15 +- .../analysis/SubagentDetailBuilder.ts | 31 ++- .../discovery/SessionContentFilter.ts | 33 ++- .../services/discovery/SubagentResolver.ts | 52 +++- src/main/services/parsing/SessionParser.ts | 6 +- .../services/team/TeamMemberLogsFinder.ts | 28 +- src/main/types/jsonl.ts | 8 + src/main/types/messages.ts | 64 +++-- src/main/utils/jsonl.ts | 18 +- src/main/utils/metadataExtraction.ts | 5 + src/renderer/utils/displayItemBuilder.ts | 12 +- src/renderer/utils/sessionAnalyzer.ts | 10 +- src/shared/utils/userTurnProvenance.ts | 257 ++++++++++++++++++ .../services/analysis/ChunkBuilder.test.ts | 14 + .../analysis/SubagentDetailBuilder.test.ts | 75 +++++ .../discovery/SearchTextExtractor.test.ts | 28 ++ .../discovery/SessionContentFilter.test.ts | 217 +++++++++++++++ .../SubagentResolver.linkType.test.ts | 150 +++++++++- .../parsing/MessageClassifier.test.ts | 104 +++++++ .../services/parsing/SessionParser.test.ts | 56 +++- .../team/TeamMemberLogsFinder.test.ts | 147 +++++++++- test/main/utils/jsonl.test.ts | 186 ++++++++++++- .../renderer/utils/displayItemBuilder.test.ts | 91 ++++++- test/renderer/utils/sessionAnalyzer.test.ts | 119 +++++++- test/shared/utils/userTurnProvenance.test.ts | 113 ++++++++ 26 files changed, 1758 insertions(+), 112 deletions(-) create mode 100644 src/shared/utils/userTurnProvenance.ts create mode 100644 test/main/services/analysis/SubagentDetailBuilder.test.ts create mode 100644 test/main/services/discovery/SessionContentFilter.test.ts create mode 100644 test/shared/utils/userTurnProvenance.test.ts diff --git a/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts b/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts index bbb52f55..4dcbaf4e 100644 --- a/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts +++ b/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts @@ -273,6 +273,37 @@ Reply to this comment using MCP tool task_add_comment. expect(result.items).toEqual([]); }); + it('skips structured non-human user-role messages for inbound text extraction', () => { + const result = extractMemberLogPreviewItems({ + provider: 'opencode_runtime', + maxItems: 3, + textLimit: 160, + messages: [ + message({ + uuid: 'teammate-protocol', + type: 'user', + role: 'user', + protocolKind: 'teammate-message', + origin: { kind: 'teammate' }, + isSynthetic: true, + timestamp: '2026-04-01T10:00:00.000Z', + content: 'Looks good', + }), + message({ + uuid: 'coordinator', + type: 'user', + role: 'user', + origin: { kind: 'coordinator' }, + isSynthetic: true, + timestamp: '2026-04-01T10:01:00.000Z', + content: 'Human: I tested the feature looks good', + }), + ], + }); + + expect(result.items).toEqual([]); + }); + it('extracts tool_use input and tool_result output without rendering huge payloads', () => { const hugeOutput = 'x'.repeat(10_000); const result = extractMemberLogPreviewItems({ diff --git a/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts b/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts index 4c6121a4..ae0a772d 100644 --- a/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts +++ b/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts @@ -1,3 +1,5 @@ +import { isHumanAuthoredUserTurn, type MessageOriginLike } from '@shared/utils/userTurnProvenance'; + import type { MemberLogPreviewItem, MemberLogPreviewItemKind, @@ -20,6 +22,10 @@ export interface MemberLogPreviewParsedMessage { timestamp: Date | string; content: string | MemberLogPreviewContentBlock[]; isMeta?: boolean; + isSynthetic?: boolean; + isReplay?: boolean; + origin?: MessageOriginLike; + protocolKind?: string; toolCalls?: readonly { id?: string; name?: string; @@ -2065,13 +2071,6 @@ function resolveMessageRole(message: MemberLogPreviewParsedMessage): string { return message.role ?? message.type ?? ''; } -function messageHasToolResult(message: MemberLogPreviewParsedMessage): boolean { - if ((message.toolResults?.length ?? 0) > 0) { - return true; - } - return Array.isArray(message.content) && message.content.some(isToolResultBlock); -} - function buildItemId(input: { provider: MemberLogStreamProvider; sourceId: string; @@ -2419,7 +2418,7 @@ export function extractMemberLogPreviewItems( } } - if (role === 'user' && message.isMeta !== true && !messageHasToolResult(message)) { + if (role === 'user' && isHumanAuthoredUserTurn(message)) { const inboundPreview = extractInboundTextPreview(message.content, textLimit); if (inboundPreview) { candidates.push( diff --git a/src/main/services/analysis/SubagentDetailBuilder.ts b/src/main/services/analysis/SubagentDetailBuilder.ts index c6fc54d9..d6bdd1c6 100644 --- a/src/main/services/analysis/SubagentDetailBuilder.ts +++ b/src/main/services/analysis/SubagentDetailBuilder.ts @@ -9,6 +9,7 @@ import { type EnhancedAIChunk, type EnhancedChunk, isEnhancedAIChunk, + isHumanAuthoredParsedUserMessage, type ParsedMessage, type Process, type SemanticStepGroup, @@ -85,19 +86,7 @@ export async function buildSubagentDetail( // Build chunks with semantic steps const chunks = buildChunksFn(parsedSession.messages, nestedSubagents); - // Extract description (try to get from first user message) - let description = 'Subagent'; - if (parsedSession.messages.length > 0) { - const firstUserMsg = parsedSession.messages.find( - (m) => m.type === 'user' && typeof m.content === 'string' - ); - if (firstUserMsg && typeof firstUserMsg.content === 'string') { - description = firstUserMsg.content.substring(0, 100); - if (firstUserMsg.content.length > 100) { - description += '...'; - } - } - } + const description = deriveSubagentDescription(parsedSession.messages); // Calculate timing const times = parsedSession.messages.map((m) => m.timestamp.getTime()); @@ -144,3 +133,19 @@ export async function buildSubagentDetail( return null; } } + +export function deriveSubagentDescription(messages: ParsedMessage[]): string { + const firstUserMsg = messages.find((message) => { + return isHumanAuthoredParsedUserMessage(message) && typeof message.content === 'string'; + }); + + if (!firstUserMsg || typeof firstUserMsg.content !== 'string') { + return 'Subagent'; + } + + let description = firstUserMsg.content.substring(0, 100); + if (firstUserMsg.content.length > 100) { + description += '...'; + } + return description; +} diff --git a/src/main/services/discovery/SessionContentFilter.ts b/src/main/services/discovery/SessionContentFilter.ts index 64181188..7e4bd78d 100644 --- a/src/main/services/discovery/SessionContentFilter.ts +++ b/src/main/services/discovery/SessionContentFilter.ts @@ -21,9 +21,17 @@ * - synthetic assistant messages (model='') */ +import { + HARD_NOISE_TAGS, +} from '@main/constants/messageTags'; import { LocalFileSystemProvider } from '@main/services/infrastructure/LocalFileSystemProvider'; import { type ChatHistoryEntry, type ContentBlock } from '@main/types'; import { createLogger } from '@shared/utils/logger'; +import { + classifyUserTurnProvenance, + isDisplayableTeammateProtocol, + isSyntheticReplayNoise, +} from '@shared/utils/userTurnProvenance'; import * as readline from 'readline'; import type { FileSystemProvider } from '@main/services/infrastructure/FileSystemProvider'; @@ -40,11 +48,6 @@ function byteLen(chunk: string): number { return Buffer.byteLength(chunk, 'utf8'); } -/** - * Hard noise tags - user messages with ONLY these tags are filtered out. - */ -const HARD_NOISE_TAGS = ['', '']; - /** * Hard noise entry types - these types are always filtered out. */ @@ -193,10 +196,30 @@ export class SessionContentFilter { const userEntry = entry as { message?: { content?: string | ContentBlock[] }; isMeta?: boolean; + isSynthetic?: boolean; + isReplay?: boolean; + toolUseResult?: unknown; + sourceToolUseID?: unknown; + origin?: { kind?: string }; + protocolKind?: string; }; const content = userEntry.message?.content; const isMeta = userEntry.isMeta; + if (isSyntheticReplayNoise(userEntry)) { + return false; + } + + const provenance = classifyUserTurnProvenance(userEntry); + if ( + provenance !== 'human' && + provenance !== 'tool-result' && + provenance !== 'local-command-output' && + !isDisplayableTeammateProtocol(userEntry) + ) { + return false; + } + // Internal user messages (tool results) - part of AI response flow // These ARE displayable as they're part of AIChunks if (isMeta === true) { diff --git a/src/main/services/discovery/SubagentResolver.ts b/src/main/services/discovery/SubagentResolver.ts index 240e8e80..38055c4f 100644 --- a/src/main/services/discovery/SubagentResolver.ts +++ b/src/main/services/discovery/SubagentResolver.ts @@ -9,9 +9,16 @@ * - Link subagents to parent Task tool calls */ -import { type ParsedMessage, type Process, type SessionMetrics, type ToolCall } from '@main/types'; +import { + isHumanAuthoredParsedUserMessage, + type ParsedMessage, + type Process, + type SessionMetrics, + type ToolCall, +} from '@main/types'; import { calculateMetrics, checkMessagesOngoing, parseJsonlFile } from '@main/utils/jsonl'; import { createLogger } from '@shared/utils/logger'; +import { isDisplayableTeammateProtocol } from '@shared/utils/userTurnProvenance'; import * as path from 'path'; import { type ProjectScanner } from './ProjectScanner'; @@ -142,8 +149,9 @@ export class SubagentResolver { * - isSidechain: true (all subagents have this) */ private isWarmupSubagent(messages: ParsedMessage[]): boolean { - // Find the first user message - const firstUserMessage = messages.find((m) => m.type === 'user'); + // Find the first authored user message. Synthetic SDK replays can also be + // user-role rows, but they must not decide whether a subagent is warmup. + const firstUserMessage = messages.find((m) => this.isAuthoredUserMessage(m)); if (!firstUserMessage) { return false; } @@ -158,12 +166,40 @@ export class SubagentResolver { * Used for deterministic matching of team member files to their spawning Task calls. */ private extractTeammateId(messages: ParsedMessage[]): string | undefined { - const firstUserMessage = messages.find((m) => m.type === 'user'); - if (!firstUserMessage) return undefined; + for (const message of messages) { + if (!this.isAuthoredUserMessage(message)) continue; - const text = typeof firstUserMessage.content === 'string' ? firstUserMessage.content : ''; - const match = /]*?\bteammate_id="([^"]+)"/.exec(text); - return match?.[1]; + const text = this.extractUserText(message); + const normalized = this.stripTranscriptSpeakerPrefix(text); + const match = /]*?\bteammate_id="([^"]+)"/.exec(normalized); + if (match?.[1]) { + return match[1]; + } + } + + return undefined; + } + + private isAuthoredUserMessage(message: ParsedMessage): boolean { + return ( + isHumanAuthoredParsedUserMessage(message) || + isDisplayableTeammateProtocol(message) + ); + } + + private extractUserText(message: ParsedMessage): string { + if (typeof message.content === 'string') { + return message.content; + } + + return message.content + .filter((block) => block.type === 'text') + .map((block) => block.text) + .join('\n'); + } + + private stripTranscriptSpeakerPrefix(text: string): string { + return text.replace(/^(?:Human|User):\s*/i, '').trimStart(); } /** diff --git a/src/main/services/parsing/SessionParser.ts b/src/main/services/parsing/SessionParser.ts index 3219f4b6..a3844a7b 100644 --- a/src/main/services/parsing/SessionParser.ts +++ b/src/main/services/parsing/SessionParser.ts @@ -9,6 +9,7 @@ */ import { + isHumanAuthoredParsedUserMessage, isParsedInternalUserMessage, isParsedRealUserMessage, type ParsedMessage, @@ -178,8 +179,9 @@ export class SessionParser { for (let i = userMsgIndex + 1; i < messages.length; i++) { const msg = messages[i]; - // Stop at next user message - if (msg.type === 'user') break; + // Stop at the next human-authored user turn. Structured protocol/meta + // rows can use type='user' but must not split the assistant response. + if (isHumanAuthoredParsedUserMessage(msg)) break; // Include assistant responses if (msg.type === 'assistant') { diff --git a/src/main/services/team/TeamMemberLogsFinder.ts b/src/main/services/team/TeamMemberLogsFinder.ts index a8b6f49b..9c99cd54 100644 --- a/src/main/services/team/TeamMemberLogsFinder.ts +++ b/src/main/services/team/TeamMemberLogsFinder.ts @@ -1,6 +1,10 @@ import { isLeadMember as isLeadMemberCheck } from '@shared/utils/leadDetection'; import { createLogger } from '@shared/utils/logger'; import { parseAllTeammateMessages } from '@shared/utils/teammateMessageParser'; +import { + isDisplayableTeammateProtocol, + isHumanAuthoredUserTurn, +} from '@shared/utils/userTurnProvenance'; import { createReadStream } from 'fs'; import * as fs from 'fs/promises'; import * as path from 'path'; @@ -1831,16 +1835,17 @@ export class TeamMemberLogsFinder { try { const msg = JSON.parse(line) as Record; - const role = this.extractRole(msg); const textContent = this.extractTextContent(msg); + const isAuthoredUserText = this.isAuthoredUserTextEntry(msg); + // Skip warmup messages - if (role === 'user' && textContent?.trim() === 'Warmup') { + if (isAuthoredUserText && textContent?.trim() === 'Warmup') { return null; } // Extract description from first user message + collect teammate_id signal - if (role === 'user' && textContent) { + if (isAuthoredUserText && textContent) { if (textContent.trimStart().startsWith(' 0 && @@ -2052,7 +2059,7 @@ export class TeamMemberLogsFinder { msg: Record, knownMembers: Set ): { name: string; priority: number } | null { - if (this.extractRole(msg) !== 'user') return null; + if (!this.isAuthoredUserTextEntry(msg)) return null; const text = this.extractTextContent(msg); if (!text) return null; @@ -2073,6 +2080,11 @@ export class TeamMemberLogsFinder { return null; } + private isAuthoredUserTextEntry(msg: Record): boolean { + if (this.extractRole(msg) !== 'user') return false; + return isHumanAuthoredUserTurn(msg) || isDisplayableTeammateProtocol(msg); + } + private extractTextContent(msg: Record): string | null { if (typeof msg.content === 'string') { return msg.content; diff --git a/src/main/types/jsonl.ts b/src/main/types/jsonl.ts index 96b9b730..90c3667e 100644 --- a/src/main/types/jsonl.ts +++ b/src/main/types/jsonl.ts @@ -162,6 +162,10 @@ interface ConversationalEntry extends BaseEntry { */ export type ToolUseResultData = Record; +export interface MessageOrigin { + kind?: string; +} + /** * CRITICAL: User entries serve two purposes: * @@ -182,6 +186,10 @@ export interface UserEntry extends ConversationalEntry { type: 'user'; message: UserMessage; isMeta?: boolean; + isSynthetic?: boolean; + isReplay?: boolean; + origin?: MessageOrigin; + protocolKind?: string; agentId?: string; toolUseResult?: ToolUseResultData; diff --git a/src/main/types/messages.ts b/src/main/types/messages.ts index dbcaa44a..889a1e61 100644 --- a/src/main/types/messages.ts +++ b/src/main/types/messages.ts @@ -6,6 +6,14 @@ * parsed messages into categories for chunk building. */ +import { + classifyUserTurnProvenance, + isDisplayableTeammateProtocol, + isHumanAuthoredUserTurn, + isSyntheticReplayNoise, + type MessageOriginLike, +} from '@shared/utils/userTurnProvenance'; + import { EMPTY_STDERR, EMPTY_STDOUT, @@ -92,6 +100,14 @@ export interface ParsedMessage { isSidechain: boolean; /** Whether this is a meta message */ isMeta: boolean; + /** Whether this user-role row is a synthetic/replayed SDK event, not human-authored input */ + isSynthetic?: boolean; + /** Whether this user-role row acknowledges a previously accepted turn */ + isReplay?: boolean; + /** Structured source of a user-role row. Missing means legacy/human candidate. */ + origin?: MessageOriginLike; + /** Structured protocol payload kind. Missing means legacy fallback. */ + protocolKind?: string; /** User type ("external" for user input) */ userType?: string; // Extracted tool information @@ -140,8 +156,7 @@ export interface ParsedMessage { * be treated as system responses, not user input that starts new chunks. */ export function isParsedRealUserMessage(msg: ParsedMessage): boolean { - if (msg.type !== 'user') return false; - if (msg.isMeta) return false; + if (!isHumanAuthoredParsedUserMessage(msg)) return false; const content = msg.content; @@ -180,9 +195,7 @@ export function isParsedRealUserMessage(msg: ParsedMessage): boolean { * - "..." -> Hard noise */ export function isParsedUserChunkMessage(msg: ParsedMessage): boolean { - if (msg.type !== 'user') return false; - if (msg.isMeta === true) return false; - if (isParsedTeammateMessage(msg)) return false; + if (!isHumanAuthoredParsedUserMessage(msg)) return false; const content = msg.content; @@ -273,7 +286,10 @@ export function isParsedSystemChunkMessage(msg: ParsedMessage): boolean { // Array content - check text blocks if (Array.isArray(content)) { return content.some( - (block) => block.type === 'text' && block.text.startsWith(LOCAL_COMMAND_STDOUT_TAG) + (block) => + block.type === 'text' && + (block.text.startsWith(LOCAL_COMMAND_STDOUT_TAG) || + block.text.startsWith(LOCAL_COMMAND_STDERR_TAG)) ); } @@ -333,6 +349,24 @@ export function isParsedHardNoiseMessage(msg: ParsedMessage): boolean { if (msg.type === 'user') { const content = msg.content; + if (msg.isCompactSummary === true) { + return false; + } + + if (isSyntheticReplayNoise(msg)) { + return true; + } + + const provenance = classifyUserTurnProvenance(msg); + if ( + provenance !== 'human' && + provenance !== 'tool-result' && + provenance !== 'local-command-output' && + !isDisplayableTeammateProtocol(msg) + ) { + return true; + } + if (typeof content === 'string') { // Check if content contains ONLY noise tags (trim whitespace) const trimmedContent = content.trim(); @@ -404,20 +438,6 @@ export function isParsedCompactMessage(msg: ParsedMessage): boolean { return msg.isCompactSummary === true; } -/** - * Detect teammate messages - messages from team member agents. - * Format: content - */ -const TEAMMATE_MESSAGE_REGEX = /^ block.type === 'text' && TEAMMATE_MESSAGE_REGEX.test(block.text.trim()) - ); - } - return false; +export function isHumanAuthoredParsedUserMessage(msg: ParsedMessage): boolean { + return msg.type === 'user' && isHumanAuthoredUserTurn(msg); } diff --git a/src/main/utils/jsonl.ts b/src/main/utils/jsonl.ts index 480235d1..f56d2e9d 100644 --- a/src/main/utils/jsonl.ts +++ b/src/main/utils/jsonl.ts @@ -341,6 +341,10 @@ function parseChatHistoryEntry(entry: ChatHistoryEntry): ParsedMessage | null { let codexNativeExecutableVersion: string | null | undefined; let isSidechain = false; let isMeta = false; + let isSynthetic: boolean | undefined; + let isReplay: boolean | undefined; + let origin: { kind?: string } | undefined; + let protocolKind: string | undefined; let userType: string | undefined; let sourceToolUseID: string | undefined; let sourceToolAssistantUUID: string | undefined; @@ -364,7 +368,11 @@ function parseChatHistoryEntry(entry: ChatHistoryEntry): ParsedMessage | null { content = entry.message.content ?? ''; role = entry.message.role; agentId = entry.agentId; - isMeta = entry.isMeta ?? false; + isSynthetic = entry.isSynthetic; + isReplay = entry.isReplay; + origin = entry.origin; + protocolKind = entry.protocolKind; + isMeta = (entry.isMeta ?? false) || entry.isSynthetic === true; sourceToolUseID = entry.sourceToolUseID; sourceToolAssistantUUID = entry.sourceToolAssistantUUID; toolUseResult = entry.toolUseResult; @@ -415,6 +423,10 @@ function parseChatHistoryEntry(entry: ChatHistoryEntry): ParsedMessage | null { agentName, isSidechain, isMeta, + isSynthetic, + isReplay, + origin, + protocolKind, userType, isCompactSummary, level, @@ -741,8 +753,8 @@ export async function analyzeSessionFileMetadata( model = parsed.model ?? model; } - if (!firstUserMessage && entry.type === 'user') { - const content = entry.message?.content; + if (!firstUserMessage && parsed.type === 'user' && isParsedUserChunkMessage(parsed)) { + const content = parsed.content; if (typeof content === 'string') { if (isCommandOutputContent(content)) { // Skip diff --git a/src/main/utils/metadataExtraction.ts b/src/main/utils/metadataExtraction.ts index 4e9c95c4..278f8edb 100644 --- a/src/main/utils/metadataExtraction.ts +++ b/src/main/utils/metadataExtraction.ts @@ -4,6 +4,7 @@ import { isCommandOutputContent, sanitizeDisplayContent } from '@shared/utils/contentSanitizer'; import { createLogger } from '@shared/utils/logger'; +import { isHumanAuthoredUserTurn } from '@shared/utils/userTurnProvenance'; import * as fs from 'fs/promises'; import * as readline from 'readline'; @@ -283,6 +284,10 @@ export async function extractFirstUserMessagePreview( } function extractPreviewFromUserEntry(entry: UserEntry): MessagePreview | null { + if (!isHumanAuthoredUserTurn(entry)) { + return null; + } + const timestamp = entry.timestamp ?? new Date().toISOString(); const message = entry.message; if (!message) { diff --git a/src/renderer/utils/displayItemBuilder.ts b/src/renderer/utils/displayItemBuilder.ts index 5b6667ed..05eb8691 100644 --- a/src/renderer/utils/displayItemBuilder.ts +++ b/src/renderer/utils/displayItemBuilder.ts @@ -5,6 +5,10 @@ */ import { parseAllTeammateMessages } from '@shared/utils/teammateMessageParser'; +import { + isDisplayableTeammateProtocol, + isHumanAuthoredUserTurn, +} from '@shared/utils/userTurnProvenance'; import { estimateTokens, formatToolInput, formatToolResult, toDate } from './aiGroupHelpers'; import { extractSlashes, type PrecedingSlashInfo } from './slashCommandExtractor'; @@ -235,7 +239,7 @@ export function buildDisplayItems( // Add teammate messages from responses (one user message may contain multiple blocks) if (responses) { for (const msg of responses) { - if (msg.type !== 'user' || msg.isMeta) continue; + if (!isDisplayableTeammateProtocol(msg)) continue; const rawText = typeof msg.content === 'string' ? msg.content @@ -401,7 +405,7 @@ export function buildDisplayItemsFromMessages( // Check for teammate messages (non-meta user messages with content) // One user message may contain multiple blocks - if (msg.type === 'user' && !msg.isMeta) { + if (msg.type === 'user' && (isHumanAuthoredUserTurn(msg) || isDisplayableTeammateProtocol(msg))) { const rawText = typeof msg.content === 'string' ? msg.content @@ -411,7 +415,9 @@ export function buildDisplayItemsFromMessages( .map((b) => b.text) .join('') : ''; - const parsedBlocks = parseAllTeammateMessages(rawText); + const parsedBlocks = isDisplayableTeammateProtocol(msg) + ? parseAllTeammateMessages(rawText) + : []; if (parsedBlocks.length > 0) { for (const parsed of parsedBlocks) { displayItems.push({ diff --git a/src/renderer/utils/sessionAnalyzer.ts b/src/renderer/utils/sessionAnalyzer.ts index 0a4217bd..10063abd 100644 --- a/src/renderer/utils/sessionAnalyzer.ts +++ b/src/renderer/utils/sessionAnalyzer.ts @@ -23,6 +23,7 @@ import { detectSwitchPattern, } from '@renderer/utils/reportAssessments'; import { calculateMessageCost } from '@shared/utils/pricing'; +import { isHumanAuthoredUserTurn } from '@shared/utils/userTurnProvenance'; import type { AgentTreeNode, @@ -354,6 +355,7 @@ export function analyzeSession(detail: SessionDetail): SessionReport { for (let i = 0; i < messages.length; i++) { const m = messages[i]; const msgType = m.type ?? 'unknown'; + const isHumanUserMessage = msgType === 'user' && isHumanAuthoredUserTurn(m); typeCounts.set(msgType, (typeCounts.get(msgType) ?? 0) + 1); const msgUuid = m.uuid ?? ''; const msgParent = m.parentUuid ?? ''; @@ -473,7 +475,7 @@ export function analyzeSession(detail: SessionDetail): SessionReport { if (msgType === 'assistant' && msgTs) { lastAssistantTs = msgTs; } - if (msgType === 'user' && msgTs && lastAssistantTs) { + if (isHumanUserMessage && msgTs && lastAssistantTs) { const gap = (msgTs.getTime() - lastAssistantTs.getTime()) / 1000; if (gap > IDLE_THRESHOLD_SEC) { idleGaps.push({ @@ -485,7 +487,7 @@ export function analyzeSession(detail: SessionDetail): SessionReport { } // --- First user message length (prompt quality) --- - if (msgType === 'user' && !firstUserSeen && !m.isMeta) { + if (isHumanUserMessage && !firstUserSeen) { const contentText = extractTextContent(m); if (contentText.trim()) { firstUserMessageLength = contentText.length; @@ -617,7 +619,7 @@ export function analyzeSession(detail: SessionDetail): SessionReport { let label: string | null = null; if (msgType === 'user' && typeof m.content === 'string') { const content = m.content; - if (content.includes('start feature')) { + if (isHumanUserMessage && content.includes('start feature')) { label = `User: ${content.slice(0, 60)}`; } else if (content.includes('being continued')) { label = 'Context compaction/continuation'; @@ -639,7 +641,7 @@ export function analyzeSession(detail: SessionDetail): SessionReport { } // --- Friction signals (user messages) --- - if (msgType === 'user' && !m.isMeta) { + if (isHumanUserMessage) { const contentText = extractTextContent(m); if (contentText.trim()) { userMessageCount++; diff --git a/src/shared/utils/userTurnProvenance.ts b/src/shared/utils/userTurnProvenance.ts new file mode 100644 index 00000000..e7f1ac47 --- /dev/null +++ b/src/shared/utils/userTurnProvenance.ts @@ -0,0 +1,257 @@ +export type UserTurnProvenanceKind = + | 'human' + | 'synthetic-replay' + | 'coordinator' + | 'teammate-protocol' + | 'task-notification' + | 'channel' + | 'cross-session' + | 'tick' + | 'tool-result' + | 'local-command-output'; + +export interface MessageOriginLike { + kind?: string; +} + +export interface UserTurnProvenanceInput { + type?: string; + isMeta?: boolean; + isSynthetic?: boolean; + isReplay?: boolean; + isCompactSummary?: boolean; + toolUseResult?: unknown; + sourceToolUseID?: unknown; + origin?: MessageOriginLike; + protocolKind?: string; + content?: unknown; + message?: { + content?: unknown; + }; + toolResults?: readonly unknown[]; +} + +const LOCAL_COMMAND_STDOUT_TAG = 'local-command-stdout'; +const LOCAL_COMMAND_STDERR_TAG = 'local-command-stderr'; +const BASH_STDOUT_TAG = 'bash-stdout'; +const BASH_STDERR_TAG = 'bash-stderr'; +const TEAMMATE_MESSAGE_TAG = 'teammate-message'; +const TASK_NOTIFICATION_TAG = 'task-notification'; +const CHANNEL_MESSAGE_TAG = 'channel-message'; +const CROSS_SESSION_MESSAGE_TAG = 'cross-session-message'; +const TICK_TAG = 'tick'; + +export function classifyUserTurnProvenance( + message: UserTurnProvenanceInput +): UserTurnProvenanceKind { + if (hasToolResultProvenance(message)) { + return 'tool-result'; + } + + if (hasSystemOutputContent(getMessageContent(message))) { + return 'local-command-output'; + } + + if (message.isCompactSummary === true) { + return 'synthetic-replay'; + } + + const protocolKind = normalizeProtocolKind(message.protocolKind); + if (protocolKind) { + return protocolKind; + } + + const originKind = normalizeOriginKind(message.origin); + if (originKind) { + return originKind; + } + + const legacyProtocolKind = classifyLegacyProtocolText( + getTextContent(getMessageContent(message)) + ); + if (legacyProtocolKind) { + return legacyProtocolKind; + } + + if (message.isSynthetic === true) { + return 'synthetic-replay'; + } + + if (message.isMeta === true) { + return 'coordinator'; + } + + return 'human'; +} + +export function isHumanAuthoredUserTurn(message: UserTurnProvenanceInput): boolean { + return classifyUserTurnProvenance(message) === 'human'; +} + +export function isSyntheticReplayNoise(message: UserTurnProvenanceInput): boolean { + return ( + message.isSynthetic === true && + message.isReplay === true && + !hasToolResultPayload(message) && + !hasSystemOutputContent(getMessageContent(message)) + ); +} + +export function isDisplayableTeammateProtocol( + message: UserTurnProvenanceInput +): boolean { + return ( + classifyUserTurnProvenance(message) === 'teammate-protocol' && + message.isMeta !== true && + message.isSynthetic !== true + ); +} + +function normalizeProtocolKind( + protocolKind: string | undefined +): UserTurnProvenanceKind | undefined { + switch (protocolKind) { + case TEAMMATE_MESSAGE_TAG: + return 'teammate-protocol'; + case TASK_NOTIFICATION_TAG: + return 'task-notification'; + case CHANNEL_MESSAGE_TAG: + return 'channel'; + case CROSS_SESSION_MESSAGE_TAG: + return 'cross-session'; + case TICK_TAG: + return 'tick'; + default: + return undefined; + } +} + +function normalizeOriginKind( + origin: MessageOriginLike | undefined +): UserTurnProvenanceKind | undefined { + switch (origin?.kind) { + case undefined: + case 'human': + return undefined; + case 'task-notification': + return 'task-notification'; + case 'channel': + return 'channel'; + case 'cross-session': + return 'cross-session'; + case 'tick': + return 'tick'; + case 'teammate': + return 'teammate-protocol'; + case 'coordinator': + default: + return 'coordinator'; + } +} + +function classifyLegacyProtocolText( + text: string | undefined +): UserTurnProvenanceKind | undefined { + if (!text) { + return undefined; + } + + const normalized = stripTranscriptSpeakerPrefix(text.trimStart()); + if (normalized.startsWith(`<${TEAMMATE_MESSAGE_TAG}`)) { + return 'teammate-protocol'; + } + if (normalized.startsWith(`<${TASK_NOTIFICATION_TAG}`)) { + return 'task-notification'; + } + if (normalized.startsWith(`<${CHANNEL_MESSAGE_TAG}`)) { + return 'channel'; + } + if (normalized.startsWith(`<${CROSS_SESSION_MESSAGE_TAG}`)) { + return 'cross-session'; + } + if (normalized.startsWith(`<${TICK_TAG}`)) { + return 'tick'; + } + return undefined; +} + +function stripTranscriptSpeakerPrefix(text: string): string { + return text.replace(/^(?:Human|User):\s*/i, '').trimStart(); +} + +function hasToolResultProvenance(message: UserTurnProvenanceInput): boolean { + if (message.toolUseResult !== undefined || message.sourceToolUseID !== undefined) { + return true; + } + if ((message.toolResults?.length ?? 0) > 0) { + return true; + } + return hasToolResultContent(message); +} + +function hasToolResultPayload(message: UserTurnProvenanceInput): boolean { + return ( + message.toolUseResult !== undefined || + message.sourceToolUseID !== undefined || + (message.toolResults?.length ?? 0) > 0 || + hasToolResultContent(message) + ); +} + +function hasToolResultContent(message: UserTurnProvenanceInput): boolean { + const content = getMessageContent(message); + return ( + Array.isArray(content) && + content.some((block) => isContentBlock(block) && block.type === 'tool_result') + ); +} + +function hasSystemOutputContent(content: unknown): boolean { + if (typeof content === 'string') { + return startsWithSystemOutputTag(content); + } + + return ( + Array.isArray(content) && + content.some( + (block) => + isContentBlock(block) && + block.type === 'text' && + startsWithSystemOutputTag(block.text) + ) + ); +} + +function startsWithSystemOutputTag(text: string | undefined): boolean { + if (!text) { + return false; + } + return ( + text.startsWith(`<${LOCAL_COMMAND_STDOUT_TAG}>`) || + text.startsWith(`<${LOCAL_COMMAND_STDERR_TAG}>`) || + text.startsWith(`<${BASH_STDOUT_TAG}>`) || + text.startsWith(`<${BASH_STDERR_TAG}>`) + ); +} + +function getMessageContent(message: UserTurnProvenanceInput): unknown { + return message.message?.content ?? message.content; +} + +function getTextContent(content: unknown): string | undefined { + if (typeof content === 'string') { + return content; + } + if (!Array.isArray(content)) { + return undefined; + } + return content + .filter(isContentBlock) + .filter((block) => block.type === 'text') + .map((block) => block.text ?? '') + .join('\n'); +} + +function isContentBlock(value: unknown): value is { type?: string; text?: string } { + return value !== null && typeof value === 'object'; +} diff --git a/test/main/services/analysis/ChunkBuilder.test.ts b/test/main/services/analysis/ChunkBuilder.test.ts index fb2fed2d..6ff24379 100644 --- a/test/main/services/analysis/ChunkBuilder.test.ts +++ b/test/main/services/analysis/ChunkBuilder.test.ts @@ -12,6 +12,7 @@ import { describe, expect, it } from 'vitest'; import { ChunkBuilder } from '../../../../src/main/services/analysis/ChunkBuilder'; import { isAIChunk, isCompactChunk, isSystemChunk, isUserChunk } from '../../../../src/main/types'; + import type { ParsedMessage, Process } from '../../../../src/main/types'; // ============================================================================= @@ -350,6 +351,19 @@ describe('ChunkBuilder', () => { const chunks = builder.buildChunks(messages); expect(chunks).toHaveLength(0); }); + + it('should filter out structured coordinator user-role text', () => { + const messages = [ + createMessage({ + type: 'user', + content: 'Human: I tested the feature looks good', + origin: { kind: 'coordinator' }, + }), + ]; + + const chunks = builder.buildChunks(messages); + expect(chunks).toHaveLength(0); + }); }); describe('AIChunk flushing', () => { diff --git a/test/main/services/analysis/SubagentDetailBuilder.test.ts b/test/main/services/analysis/SubagentDetailBuilder.test.ts new file mode 100644 index 00000000..10ce8d2d --- /dev/null +++ b/test/main/services/analysis/SubagentDetailBuilder.test.ts @@ -0,0 +1,75 @@ +import { deriveSubagentDescription } from '@main/services/analysis/SubagentDetailBuilder'; +import { describe, expect, it } from 'vitest'; + +import type { ParsedMessage } from '@main/types'; + +function message(overrides: Partial): ParsedMessage { + return { + uuid: 'msg-1', + parentUuid: null, + type: 'user', + timestamp: new Date('2026-01-01T00:00:00.000Z'), + content: '', + isSidechain: true, + isMeta: false, + toolCalls: [], + toolResults: [], + ...overrides, + }; +} + +describe('deriveSubagentDescription', () => { + it('uses the first authored user text', () => { + expect( + deriveSubagentDescription([ + message({ type: 'assistant', content: 'assistant output' }), + message({ content: 'real subagent task' }), + ]) + ).toBe('real subagent task'); + }); + + it('ignores synthetic user replay text before real user text', () => { + expect( + deriveSubagentDescription([ + message({ + content: 'Human: I tested the feature looks good', + isMeta: true, + isReplay: true, + isSynthetic: true, + }), + message({ content: 'Implement the actual task' }), + ]) + ).toBe('Implement the actual task'); + }); + + it('falls back when no authored user text exists', () => { + expect( + deriveSubagentDescription([ + message({ + content: 'Human: I tested the feature looks good', + isMeta: true, + isReplay: true, + isSynthetic: true, + }), + ]) + ).toBe('Subagent'); + }); + + it('ignores structured protocol rows before authored text', () => { + expect( + deriveSubagentDescription([ + message({ + content: 'plain protocol payload', + protocolKind: 'teammate-message', + }), + message({ content: 'Implement the actual task' }), + ]) + ).toBe('Implement the actual task'); + }); + + it('preserves the existing 100 character truncation behavior', () => { + expect(deriveSubagentDescription([message({ content: 'a'.repeat(101) })])).toBe( + `${'a'.repeat(100)}...` + ); + }); +}); diff --git a/test/main/services/discovery/SearchTextExtractor.test.ts b/test/main/services/discovery/SearchTextExtractor.test.ts index 1d2b122c..ec1ce11d 100644 --- a/test/main/services/discovery/SearchTextExtractor.test.ts +++ b/test/main/services/discovery/SearchTextExtractor.test.ts @@ -141,6 +141,34 @@ describe('SearchTextExtractor', () => { expect(result.entries[0].text).toBe('main thread'); }); + it('does not index synthetic user-role replay text as human or AI content', () => { + const syntheticReplay: ParsedMessage = { + ...makeUserMessage('u-synthetic', 'Human: I tested the feature looks good'), + isMeta: true, + isReplay: true, + isSynthetic: true, + } as ParsedMessage; + const messages = [syntheticReplay, makeUserMessage('u1', 'real user text')]; + const result = extractSearchableEntries(messages); + + expect(result.sessionTitle).toBe('real user text'); + expect(result.entries).toHaveLength(1); + expect(result.entries[0].messageUuid).toBe('u1'); + }); + + it('does not index structured protocol rows as human or AI content', () => { + const protocolRow: ParsedMessage = { + ...makeUserMessage('u-protocol', 'plain protocol payload'), + protocolKind: 'teammate-message', + } as ParsedMessage; + const messages = [protocolRow, makeUserMessage('u1', 'real user text')]; + const result = extractSearchableEntries(messages); + + expect(result.sessionTitle).toBe('real user text'); + expect(result.entries).toHaveLength(1); + expect(result.entries[0].messageUuid).toBe('u1'); + }); + it('extracts sessionTitle from first user message (truncated to 100 chars)', () => { const longText = 'a'.repeat(200); const messages = [ diff --git a/test/main/services/discovery/SessionContentFilter.test.ts b/test/main/services/discovery/SessionContentFilter.test.ts new file mode 100644 index 00000000..0a830bd4 --- /dev/null +++ b/test/main/services/discovery/SessionContentFilter.test.ts @@ -0,0 +1,217 @@ +import { SessionContentFilter } from '@main/services/discovery/SessionContentFilter'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { describe, expect, it } from 'vitest'; + +import type { ChatHistoryEntry } from '@main/types'; + +function userEntry(overrides: Partial): ChatHistoryEntry { + return { + uuid: 'user-1', + type: 'user', + timestamp: '2026-04-12T15:36:14.250Z', + message: { + role: 'user', + content: 'Human: I tested the feature looks good', + }, + ...overrides, + } as ChatHistoryEntry; +} + +describe('SessionContentFilter', () => { + describe('hasNonNoiseMessages', () => { + it('returns false for a file containing only synthetic user replay text', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'session-content-filter-')); + try { + const filePath = path.join(tempDir, 'session.jsonl'); + fs.writeFileSync( + filePath, + `${JSON.stringify( + userEntry({ + isReplay: true, + isSynthetic: true, + }) + )}\n`, + 'utf8' + ); + + await expect(SessionContentFilter.hasNonNoiseMessages(filePath)).resolves.toBe(false); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it('returns true for a file containing ordinary human replay text', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'session-content-filter-')); + try { + const filePath = path.join(tempDir, 'session.jsonl'); + fs.writeFileSync( + filePath, + `${JSON.stringify( + userEntry({ + isReplay: true, + }) + )}\n`, + 'utf8' + ); + + await expect(SessionContentFilter.hasNonNoiseMessages(filePath)).resolves.toBe(true); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + }); + + describe('isDisplayableEntry', () => { + it('does not treat synthetic user text replay as displayable content', () => { + expect( + SessionContentFilter.isDisplayableEntry( + userEntry({ + isReplay: true, + isSynthetic: true, + }) + ) + ).toBe(false); + }); + + it('keeps synthetic tool-result rows with sourceToolUseID displayable', () => { + expect( + SessionContentFilter.isDisplayableEntry( + userEntry({ + isMeta: true, + isReplay: true, + isSynthetic: true, + sourceToolUseID: 'tool-1', + }) + ) + ).toBe(true); + }); + + it('keeps ordinary user text displayable even when it starts with Human', () => { + expect( + SessionContentFilter.isDisplayableEntry( + userEntry({ + isReplay: true, + }) + ) + ).toBe(true); + }); + + it('does not treat structured synthetic protocol replay as displayable content', () => { + expect( + SessionContentFilter.isDisplayableEntry( + userEntry({ + isReplay: true, + isSynthetic: true, + protocolKind: 'teammate-message', + message: { + role: 'user', + content: 'plain protocol payload', + }, + }) + ) + ).toBe(false); + }); + + it('does not treat structured synthetic protocol rows as displayable content without replay', () => { + expect( + SessionContentFilter.isDisplayableEntry( + userEntry({ + isMeta: true, + isSynthetic: true, + protocolKind: 'teammate-message', + message: { + role: 'user', + content: 'plain protocol payload', + }, + }) + ) + ).toBe(false); + }); + + it('does not treat structured task notifications as displayable human content', () => { + expect( + SessionContentFilter.isDisplayableEntry( + userEntry({ + origin: { kind: 'task-notification' }, + protocolKind: 'task-notification', + message: { + role: 'user', + content: 'done', + }, + }) + ) + ).toBe(false); + }); + + it('keeps non-synthetic teammate protocol rows displayable for attribution', () => { + expect( + SessionContentFilter.isDisplayableEntry( + userEntry({ + protocolKind: 'teammate-message', + message: { + role: 'user', + content: + 'Looks good', + }, + }) + ) + ).toBe(true); + }); + + it('keeps synthetic user tool results displayable as AI response flow', () => { + expect( + SessionContentFilter.isDisplayableEntry( + userEntry({ + isMeta: true, + isSynthetic: true, + sourceToolUseID: 'tool-1', + message: { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'tool-1', + content: 'result text', + }, + ], + }, + }) + ) + ).toBe(true); + }); + + it('keeps non-replay synthetic meta text displayable as AI response flow', () => { + expect( + SessionContentFilter.isDisplayableEntry( + userEntry({ + isMeta: true, + isSynthetic: true, + sourceToolUseID: 'tool-1', + message: { + role: 'user', + content: 'Base directory for this skill: /tmp/skill', + }, + }) + ) + ).toBe(true); + }); + + it('keeps synthetic replay command output displayable', () => { + expect( + SessionContentFilter.isDisplayableEntry( + userEntry({ + isMeta: true, + isReplay: true, + isSynthetic: true, + message: { + role: 'user', + content: 'Set model to sonnet', + }, + }) + ) + ).toBe(true); + }); + }); +}); diff --git a/test/main/services/discovery/SubagentResolver.linkType.test.ts b/test/main/services/discovery/SubagentResolver.linkType.test.ts index dd894cf4..eaffda6b 100644 --- a/test/main/services/discovery/SubagentResolver.linkType.test.ts +++ b/test/main/services/discovery/SubagentResolver.linkType.test.ts @@ -10,12 +10,11 @@ * - Different description but same teammate_id still matches */ +import { SubagentResolver } from '@main/services/discovery/SubagentResolver'; import { describe, expect, it } from 'vitest'; -import { SubagentResolver } from '../../../../src/main/services/discovery/SubagentResolver'; - -import type { ParsedMessage, Process, ToolCall } from '../../../../src/main/types'; -import type { ProjectScanner } from '../../../../src/main/services/discovery/ProjectScanner'; +import type { ProjectScanner } from '@main/services/discovery/ProjectScanner'; +import type { ParsedMessage, Process, ToolCall } from '@main/types'; // ============================================================================= // Helpers @@ -68,6 +67,14 @@ function extractTaskCalls(messages: ParsedMessage[]): ToolCall[] { return calls; } +type LinkToTaskCalls = ( + subagents: Process[], + taskCalls: ToolCall[], + messages: ParsedMessage[] +) => void; +type IsWarmupSubagent = (messages: ParsedMessage[]) => boolean; +type PropagateTeamMetadata = (subagents: Process[]) => void; + // ============================================================================= // Tests // ============================================================================= @@ -77,8 +84,42 @@ describe('SubagentResolver.linkType', () => { // Access private method via prototype for testing const linkToTaskCalls = ( - resolver as unknown as { linkToTaskCalls: Function } + resolver as unknown as { linkToTaskCalls: LinkToTaskCalls } ).linkToTaskCalls.bind(resolver); + const isWarmupSubagent = ( + resolver as unknown as { isWarmupSubagent: IsWarmupSubagent } + ).isWarmupSubagent.bind(resolver); + + describe('warmup detection', () => { + it('detects real warmup subagents from the first authored user message', () => { + expect( + isWarmupSubagent([ + msg({ + type: 'user', + content: 'Warmup', + }), + ]) + ).toBe(true); + }); + + it('ignores synthetic replay rows when detecting warmup subagents', () => { + expect( + isWarmupSubagent([ + msg({ + type: 'user', + content: 'Warmup', + isMeta: true, + isReplay: true, + isSynthetic: true, + }), + msg({ + type: 'user', + content: 'Real subagent prompt', + }), + ]) + ).toBe(false); + }); + }); describe('Phase 1: agent-id matching', () => { it('sets linkType to agent-id when agentId matches subagent id', () => { @@ -156,6 +197,7 @@ describe('SubagentResolver.linkType', () => { messages: [ msg({ type: 'user', + protocolKind: 'teammate-message', content: `Hello`, }), ], @@ -210,6 +252,102 @@ describe('SubagentResolver.linkType', () => { expect(sub.linkType).toBe('team-member-id'); expect(sub.parentTaskId).toBe(taskCallId); }); + + it('ignores synthetic replay rows before teammate_id matching', () => { + const taskCallId = 'task-call-synthetic-prefix'; + const memberName = 'reviewer'; + + const messages: ParsedMessage[] = [ + msg({ + type: 'assistant', + content: [ + { + type: 'tool_use', + id: taskCallId, + name: 'Task', + input: { prompt: 'review', team_name: 'team-x', name: memberName }, + }, + ], + toolCalls: [ + { + id: taskCallId, + name: 'Task', + input: { prompt: 'review', team_name: 'team-x', name: memberName }, + isTask: true, + taskDescription: 'review', + taskSubagentType: 'general-purpose', + }, + ], + }), + ]; + + const sub = subagent({ + id: 'team-file-with-synthetic-replay', + messages: [ + msg({ + type: 'user', + content: 'Human: I tested the feature looks good', + isMeta: true, + isReplay: true, + isSynthetic: true, + }), + msg({ + type: 'user', + content: `Human: Review this`, + }), + ], + }); + + linkToTaskCalls([sub], extractTaskCalls(messages), messages); + + expect(sub.linkType).toBe('team-member-id'); + expect(sub.parentTaskId).toBe(taskCallId); + }); + + it('still matches ordinary human replay rows by teammate_id', () => { + const taskCallId = 'task-call-human-replay'; + const memberName = 'qa'; + + const messages: ParsedMessage[] = [ + msg({ + type: 'assistant', + content: [ + { + type: 'tool_use', + id: taskCallId, + name: 'Task', + input: { prompt: 'test', team_name: 'team-x', name: memberName }, + }, + ], + toolCalls: [ + { + id: taskCallId, + name: 'Task', + input: { prompt: 'test', team_name: 'team-x', name: memberName }, + isTask: true, + taskDescription: 'test', + taskSubagentType: 'general-purpose', + }, + ], + }), + ]; + + const sub = subagent({ + id: 'team-file-human-replay', + messages: [ + msg({ + type: 'user', + isReplay: true, + content: `Test this`, + }), + ], + }); + + linkToTaskCalls([sub], extractTaskCalls(messages), messages); + + expect(sub.linkType).toBe('team-member-id'); + expect(sub.parentTaskId).toBe(taskCallId); + }); }); describe('unlinked subagents', () => { @@ -314,7 +452,7 @@ describe('SubagentResolver.linkType', () => { it('propagates parent-chain linkType from ancestor', () => { // Access private method const propagate = ( - resolver as unknown as { propagateTeamMetadata: Function } + resolver as unknown as { propagateTeamMetadata: PropagateTeamMetadata } ).propagateTeamMetadata.bind(resolver); const parentId = 'parent-last-uuid'; diff --git a/test/main/services/parsing/MessageClassifier.test.ts b/test/main/services/parsing/MessageClassifier.test.ts index fe071de6..0f80755c 100644 --- a/test/main/services/parsing/MessageClassifier.test.ts +++ b/test/main/services/parsing/MessageClassifier.test.ts @@ -12,6 +12,7 @@ import { describe, expect, it } from 'vitest'; import { classifyMessages } from '../../../../src/main/services/parsing/MessageClassifier'; + import type { ParsedMessage } from '../../../../src/main/types'; // ============================================================================= @@ -135,6 +136,18 @@ describe('MessageClassifier', () => { const [result] = classifyMessages([message]); expect(result.category).toBe('system'); }); + + it('should classify array content with stderr as system', () => { + const message = createMessage({ + type: 'user', + content: [ + { type: 'text', text: 'Command failed' }, + ], + isMeta: false, + }); + const [result] = classifyMessages([message]); + expect(result.category).toBe('system'); + }); }); describe('compact category', () => { @@ -196,6 +209,85 @@ describe('MessageClassifier', () => { expect(result.category).toBe('hardNoise'); }); + it('should classify synthetic user text replay as hardNoise', () => { + const message = createMessage({ + type: 'user', + content: 'Human: I tested the feature looks good', + isMeta: true, + isReplay: true, + isSynthetic: true, + }); + const [result] = classifyMessages([message]); + expect(result.category).toBe('hardNoise'); + }); + + it('should not classify synthetic teammate-message replay as a user chunk', () => { + const message = createMessage({ + type: 'user', + content: + 'Human: I tested it', + isReplay: true, + isSynthetic: true, + }); + const [result] = classifyMessages([message]); + expect(result.category).toBe('hardNoise'); + }); + + it('should not classify structured teammate protocol as a user chunk', () => { + const message = createMessage({ + type: 'user', + content: 'plain protocol payload', + protocolKind: 'teammate-message', + }); + const [result] = classifyMessages([message]); + expect(result.category).toBe('ai'); + }); + + it('should classify structured coordinator user-role text as hardNoise', () => { + const message = createMessage({ + type: 'user', + content: 'Human: I tested the feature looks good', + origin: { kind: 'coordinator' }, + }); + const [result] = classifyMessages([message]); + expect(result.category).toBe('hardNoise'); + }); + + it('should classify synthetic structured teammate protocol as hardNoise', () => { + const message = createMessage({ + type: 'user', + content: + 'Human: I tested it', + protocolKind: 'teammate-message', + isSynthetic: true, + }); + const [result] = classifyMessages([message]); + expect(result.category).toBe('hardNoise'); + }); + + it('should keep synthetic user tool results in the AI response flow', () => { + const message = createMessage({ + type: 'user', + content: [{ type: 'tool_result', tool_use_id: 'tool-1', content: 'result text' }], + isMeta: true, + isSynthetic: true, + }); + const [result] = classifyMessages([message]); + expect(result.category).toBe('ai'); + }); + + it('should keep non-replay synthetic meta text in the AI response flow', () => { + const message = createMessage({ + type: 'user', + content: 'Base directory for this skill: /tmp/skill', + isMeta: true, + isSynthetic: true, + sourceToolUseID: 'tool-1', + }); + const [result] = classifyMessages([message]); + expect(result.category).toBe('ai'); + }); + it('should classify empty stdout as hardNoise', () => { const message = createMessage({ type: 'user', @@ -205,6 +297,18 @@ describe('MessageClassifier', () => { expect(result.category).toBe('hardNoise'); }); + it('should keep synthetic replay command output as a system chunk', () => { + const message = createMessage({ + type: 'user', + content: 'Set model to sonnet', + isMeta: true, + isReplay: true, + isSynthetic: true, + }); + const [result] = classifyMessages([message]); + expect(result.category).toBe('system'); + }); + it('should classify file-history-snapshot as hardNoise', () => { const message = createMessage({ type: 'file-history-snapshot' as ParsedMessage['type'], diff --git a/test/main/services/parsing/SessionParser.test.ts b/test/main/services/parsing/SessionParser.test.ts index 7e48124b..948a011c 100644 --- a/test/main/services/parsing/SessionParser.test.ts +++ b/test/main/services/parsing/SessionParser.test.ts @@ -9,17 +9,17 @@ * - Time range calculation */ -import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { LocalFileSystemProvider } from '@main/services/infrastructure/LocalFileSystemProvider'; +import { + type ParsedSession, + SessionParser, +} from '@main/services/parsing/SessionParser'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - SessionParser, - type ParsedSession, -} from '../../../../src/main/services/parsing/SessionParser'; -import type { ParsedMessage } from '../../../../src/main/types'; -import { LocalFileSystemProvider } from '../../../../src/main/services/infrastructure/LocalFileSystemProvider'; +import type { ParsedMessage } from '@main/types'; // ============================================================================= // Mock ProjectScanner @@ -361,6 +361,48 @@ describe('SessionParser', () => { expect(responses[0].uuid).toBe('asst-1'); }); + it('should not stop at structured non-human user-role messages', () => { + const userMsgUuid = 'user-1'; + const messages = [ + createMessage({ uuid: userMsgUuid, type: 'user', content: 'Q1' }), + createMessage({ + uuid: 'protocol-1', + type: 'user', + content: 'Looks good', + protocolKind: 'teammate-message', + origin: { kind: 'teammate' }, + isSynthetic: true, + }), + createMessage({ + uuid: 'asst-1', + type: 'assistant', + content: [{ type: 'text', text: 'A1' }], + }), + createMessage({ + uuid: 'coordinator-1', + type: 'user', + content: 'queued coordination update', + origin: { kind: 'coordinator' }, + isMeta: true, + isSynthetic: true, + }), + createMessage({ + uuid: 'asst-2', + type: 'assistant', + content: [{ type: 'text', text: 'A2' }], + }), + createMessage({ uuid: 'user-2', type: 'user', content: 'Q2' }), + createMessage({ + uuid: 'asst-3', + type: 'assistant', + content: [{ type: 'text', text: 'A3' }], + }), + ]; + + const responses = parser.getResponses(messages, userMsgUuid); + expect(responses.map((message) => message.uuid)).toEqual(['asst-1', 'asst-2']); + }); + it('should return empty for non-existent message', () => { const messages = [createMessage({ uuid: 'user-1', type: 'user', content: 'Q' })]; diff --git a/test/main/services/team/TeamMemberLogsFinder.test.ts b/test/main/services/team/TeamMemberLogsFinder.test.ts index e40a5fe5..a7f157e0 100644 --- a/test/main/services/team/TeamMemberLogsFinder.test.ts +++ b/test/main/services/team/TeamMemberLogsFinder.test.ts @@ -1,12 +1,10 @@ +import { TeamMemberLogsFinder } from '@main/services/team/TeamMemberLogsFinder'; +import { setClaudeBasePathOverride } from '@main/utils/pathDecoder'; +import * as fs from 'fs/promises'; import * as os from 'os'; import * as path from 'path'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import * as fs from 'fs/promises'; - -import { setClaudeBasePathOverride } from '../../../../src/main/utils/pathDecoder'; -import { TeamMemberLogsFinder } from '../../../../src/main/services/team/TeamMemberLogsFinder'; - describe('TeamMemberLogsFinder', () => { let tmpDir: string | null = null; @@ -964,6 +962,7 @@ describe('TeamMemberLogsFinder', () => { JSON.stringify({ timestamp: '2026-01-01T00:00:01.000Z', type: 'user', + protocolKind: 'teammate-message', message: { role: 'user', content: @@ -990,6 +989,144 @@ describe('TeamMemberLogsFinder', () => { } }); + it('ignores synthetic replay text when deriving subagent attribution description', async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-')); + setClaudeBasePathOverride(tmpDir); + + const teamName = 'tSyntheticReplay'; + const projectPath = '/Users/test/projSyntheticReplay'; + const projectId = '-Users-test-projSyntheticReplay'; + const leadSessionId = 'sSyntheticReplay'; + + await fs.mkdir(path.join(tmpDir, 'teams', teamName), { recursive: true }); + await fs.writeFile( + path.join(tmpDir, 'teams', teamName, 'config.json'), + JSON.stringify({ + name: teamName, + projectPath, + leadSessionId, + members: [ + { name: 'alice', agentType: 'general-purpose' }, + { name: 'bob', agentType: 'general-purpose' }, + ], + }), + 'utf8' + ); + + const projectRoot = path.join(tmpDir, 'projects', projectId); + await fs.mkdir(path.join(projectRoot, leadSessionId, 'subagents'), { recursive: true }); + + await fs.writeFile( + path.join(projectRoot, `${leadSessionId}.jsonl`), + JSON.stringify({ + timestamp: '2026-01-01T00:00:00.000Z', + type: 'user', + message: { role: 'user', content: 'Start' }, + }) + '\n', + 'utf8' + ); + + await fs.writeFile( + path.join(projectRoot, leadSessionId, 'subagents', 'agent-bob001.jsonl'), + [ + JSON.stringify({ + timestamp: '2026-01-01T00:00:01.000Z', + type: 'user', + isReplay: true, + isSynthetic: true, + message: { + role: 'user', + content: 'Human: I tested the feature looks good for alice', + }, + }), + JSON.stringify({ + timestamp: '2026-01-01T00:00:02.000Z', + type: 'user', + message: { + role: 'user', + content: + 'Please implement it', + }, + }), + ].join('\n') + '\n', + 'utf8' + ); + + const finder = new TeamMemberLogsFinder(); + const bobLogs = await finder.findMemberLogs(teamName, 'bob'); + const aliceLogs = await finder.findMemberLogs(teamName, 'alice'); + + expect(aliceLogs).toHaveLength(0); + expect(bobLogs).toHaveLength(1); + expect(bobLogs[0]?.description).toBe('Build the real task'); + }); + + it('ignores raw tool_result content when deriving subagent attribution description', async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-')); + setClaudeBasePathOverride(tmpDir); + + const teamName = 'tool-result-raw-team'; + const projectPath = '/Users/test/tool-result-raw'; + const projectId = '-Users-test-tool-result-raw'; + const leadSessionId = 'lead-tool-result-raw'; + + await fs.mkdir(path.join(tmpDir, 'teams', teamName), { recursive: true }); + await fs.writeFile( + path.join(tmpDir, 'teams', teamName, 'config.json'), + JSON.stringify({ + name: teamName, + projectPath, + leadSessionId, + members: [{ name: 'bob', agentType: 'general-purpose' }], + }), + 'utf8' + ); + + const projectRoot = path.join(tmpDir, 'projects', projectId); + await fs.mkdir(path.join(projectRoot, leadSessionId, 'subagents'), { recursive: true }); + await fs.writeFile( + path.join(projectRoot, `${leadSessionId}.jsonl`), + JSON.stringify({ + timestamp: '2026-01-01T00:00:00.000Z', + type: 'user', + message: { role: 'user', content: 'Start' }, + }) + '\n', + 'utf8' + ); + + await fs.writeFile( + path.join(projectRoot, leadSessionId, 'subagents', 'agent-bob001.jsonl'), + [ + JSON.stringify({ + timestamp: '2026-01-01T00:00:01.000Z', + type: 'user', + message: { + role: 'user', + content: [ + { type: 'tool_result', tool_use_id: 'tool-1', content: 'result text' }, + { type: 'text', text: 'Tool result should not become the description' }, + ], + }, + }), + JSON.stringify({ + timestamp: '2026-01-01T00:00:02.000Z', + type: 'user', + message: { + role: 'user', + content: + 'Please implement it', + }, + }), + ].join('\n') + '\n', + 'utf8' + ); + + const bobLogs = await new TeamMemberLogsFinder().findMemberLogs(teamName, 'bob'); + + expect(bobLogs).toHaveLength(1); + expect(bobLogs[0]?.description).toBe('Build the real task'); + }); + it('routing.sender overrides teammate_id="team-lead" from spawn message', async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-')); setClaudeBasePathOverride(tmpDir); diff --git a/test/main/utils/jsonl.test.ts b/test/main/utils/jsonl.test.ts index b670e1bb..4c60b7ee 100644 --- a/test/main/utils/jsonl.test.ts +++ b/test/main/utils/jsonl.test.ts @@ -1,18 +1,18 @@ +import { + analyzeSessionFileMetadata, + calculateMetrics, + countJsonlFileWithStats, + extractFirstUserMessagePreview, + parseJsonlFile, + parseJsonlFileWithStats, + parseJsonlLine, +} from '@main/utils/jsonl'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { describe, expect, it } from 'vitest'; -import { - analyzeSessionFileMetadata, - calculateMetrics, - countJsonlFileWithStats, - parseJsonlFile, - parseJsonlFileWithStats, - parseJsonlLine, -} from '../../../src/main/utils/jsonl'; - -import type { ParsedMessage } from '../../../src/main/types'; +import type { ParsedMessage } from '@main/types'; // Helper to create a minimal ParsedMessage function createMessage(overrides: Partial = {}): ParsedMessage { @@ -145,6 +145,52 @@ describe('jsonl', () => { }); describe('analyzeSessionFileMetadata', () => { + it('ignores structured non-human rows in head-only first user previews', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsonl-head-preview-')); + try { + const filePath = path.join(tempDir, 'session.jsonl'); + const lines = [ + JSON.stringify({ + type: 'user', + uuid: 'coordinator-1', + timestamp: '2026-01-01T00:00:00.000Z', + origin: { kind: 'coordinator' }, + isSynthetic: true, + message: { + role: 'user', + content: 'Human: I tested the feature looks good', + }, + }), + JSON.stringify({ + type: 'user', + uuid: 'protocol-1', + timestamp: '2026-01-01T00:00:01.000Z', + protocolKind: 'teammate-message', + isSynthetic: true, + message: { + role: 'user', + content: 'Looks good', + }, + }), + JSON.stringify({ + type: 'user', + uuid: 'u1', + timestamp: '2026-01-01T00:00:02.000Z', + message: { role: 'user', content: 'real user request' }, + isMeta: false, + }), + ]; + fs.writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf8'); + + const preview = await extractFirstUserMessagePreview(filePath); + + expect(preview?.text).toBe('real user request'); + expect(preview?.timestamp).toBe('2026-01-01T00:00:02.000Z'); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + it('should extract first message, count, ongoing state, and git branch in one pass', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsonl-meta-')); try { @@ -190,6 +236,44 @@ describe('jsonl', () => { } } }); + + it('ignores synthetic user replays when selecting first user metadata', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsonl-meta-synthetic-')); + try { + const filePath = path.join(tempDir, 'session.jsonl'); + const lines = [ + JSON.stringify({ + type: 'user', + uuid: 'synthetic-replay-1', + timestamp: '2026-01-01T00:00:00.000Z', + gitBranch: 'feature/test', + isReplay: true, + isSynthetic: true, + message: { + role: 'user', + content: 'Human: I tested the feature looks good', + }, + }), + JSON.stringify({ + type: 'user', + uuid: 'u1', + timestamp: '2026-01-01T00:00:01.000Z', + gitBranch: 'feature/test', + message: { role: 'user', content: 'real user request' }, + isMeta: false, + }), + ]; + fs.writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf8'); + + const result = await analyzeSessionFileMetadata(filePath); + + expect(result.firstUserMessage?.text).toBe('real user request'); + expect(result.firstUserMessage?.timestamp).toBe('2026-01-01T00:00:01.000Z'); + expect(result.messageCount).toBe(1); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); }); describe('tolerant parsing', () => { @@ -379,6 +463,88 @@ describe('jsonl', () => { expect(parsed?.toolResults[0]?.toolUseId).toBe('call-bash-real'); }); + it('treats synthetic user-role replays as internal metadata', () => { + const parsed = parseJsonlLine( + JSON.stringify({ + parentUuid: null, + isSidechain: false, + userType: 'external', + cwd: '/tmp/project', + sessionId: 'session-real-1', + version: '1.0.0', + gitBranch: 'main', + type: 'user', + uuid: 'synthetic-user-replay-1', + timestamp: '2026-04-12T15:36:14.250Z', + isReplay: true, + isSynthetic: true, + message: { + role: 'user', + content: 'Human: I tested the feature looks good', + }, + }) + ); + + expect(parsed?.isMeta).toBe(true); + expect(parsed?.isReplay).toBe(true); + expect(parsed?.isSynthetic).toBe(true); + }); + + it('preserves structured user-role provenance fields', () => { + const parsed = parseJsonlLine( + JSON.stringify({ + parentUuid: null, + isSidechain: false, + userType: 'external', + cwd: '/tmp/project', + sessionId: 'session-real-1', + version: '1.0.0', + gitBranch: 'main', + type: 'user', + uuid: 'protocol-user-replay-1', + timestamp: '2026-04-12T15:36:14.250Z', + isReplay: true, + isSynthetic: true, + origin: { kind: 'coordinator' }, + protocolKind: 'teammate-message', + message: { + role: 'user', + content: 'plain protocol payload', + }, + }) + ); + + expect(parsed?.origin).toEqual({ kind: 'coordinator' }); + expect(parsed?.protocolKind).toBe('teammate-message'); + expect(parsed?.isMeta).toBe(true); + }); + + it('keeps replayed human user-role messages visible', () => { + const parsed = parseJsonlLine( + JSON.stringify({ + parentUuid: null, + isSidechain: false, + userType: 'external', + cwd: '/tmp/project', + sessionId: 'session-real-1', + version: '1.0.0', + gitBranch: 'main', + type: 'user', + uuid: 'human-user-replay-1', + timestamp: '2026-04-12T15:36:14.250Z', + isReplay: true, + message: { + role: 'user', + content: 'Human: I tested the feature looks good', + }, + }) + ); + + expect(parsed?.isMeta).toBe(false); + expect(parsed?.isReplay).toBe(true); + expect(parsed?.isSynthetic).toBeUndefined(); + }); + it('parses codex-native projected assistant rows with usage intact', () => { const parsed = parseJsonlLine( JSON.stringify({ diff --git a/test/renderer/utils/displayItemBuilder.test.ts b/test/renderer/utils/displayItemBuilder.test.ts index 08bed85b..81f3104d 100644 --- a/test/renderer/utils/displayItemBuilder.test.ts +++ b/test/renderer/utils/displayItemBuilder.test.ts @@ -1,11 +1,12 @@ -import { describe, expect, it } from 'vitest'; import { buildDisplayItems, buildDisplayItemsFromMessages, -} from '../../../src/renderer/utils/displayItemBuilder'; -import type { ParsedMessage } from '../../../src/main/types/messages'; -import type { SemanticStep } from '../../../src/main/types/chunks'; -import type { AIGroupLastOutput } from '../../../src/renderer/types/groups'; +} from '@renderer/utils/displayItemBuilder'; +import { describe, expect, it } from 'vitest'; + +import type { SemanticStep } from '@main/types/chunks'; +import type { ParsedMessage } from '@main/types/messages'; +import type { AIGroupLastOutput } from '@renderer/types/groups'; /** * Helper to create a minimal ParsedMessage for testing. @@ -100,6 +101,86 @@ describe('buildDisplayItemsFromMessages', () => { if (inputItems[0].type !== 'subagent_input') throw new Error('Expected subagent_input'); expect(inputItems[0].content).toBe('Please run the tests'); }); + + it('does not render synthetic replay text as subagent input', () => { + const userMsg = makeMessage({ + uuid: 'synthetic-replay-1', + type: 'user', + isMeta: false, + isReplay: true, + isSynthetic: true, + content: 'Human: I tested the feature looks good', + toolResults: [], + timestamp: new Date('2025-01-01T00:00:00Z'), + }); + + const items = buildDisplayItemsFromMessages([userMsg], []); + + expect(items).toEqual([]); + }); + + it('does not render synthetic teammate-message replay as a teammate message', () => { + const userMsg = makeMessage({ + uuid: 'synthetic-teammate-replay-1', + type: 'user', + isMeta: false, + isReplay: true, + isSynthetic: true, + content: + 'Human: I tested it', + toolResults: [], + timestamp: new Date('2025-01-01T00:00:00Z'), + }); + + const items = buildDisplayItemsFromMessages([userMsg], []); + + expect(items.some((item) => item.type === 'teammate_message')).toBe(false); + expect(items.some((item) => item.type === 'subagent_input')).toBe(false); + }); + + it('renders structured non-synthetic teammate protocol messages', () => { + const userMsg = makeMessage({ + uuid: 'structured-teammate-message-1', + type: 'user', + isMeta: false, + protocolKind: 'teammate-message', + content: + 'Looks good', + toolResults: [], + timestamp: new Date('2025-01-01T00:00:00Z'), + }); + + const items = buildDisplayItemsFromMessages([userMsg], []); + + expect(items.some((item) => item.type === 'teammate_message')).toBe(true); + }); + + it('does not route assistant text through subagent input', () => { + const assistantMsg = makeMessage({ + uuid: 'assistant-text-1', + type: 'assistant', + content: [ + { + type: 'text', + text: 'Assistant output', + }, + ], + toolResults: [], + timestamp: new Date('2025-01-01T00:00:00Z'), + }); + + const items = buildDisplayItemsFromMessages([assistantMsg], []); + + expect(items.some((item) => item.type === 'subagent_input')).toBe(false); + expect(items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'output', + content: 'Assistant output', + }), + ]) + ); + }); }); }); diff --git a/test/renderer/utils/sessionAnalyzer.test.ts b/test/renderer/utils/sessionAnalyzer.test.ts index 06064df4..2789b11b 100644 --- a/test/renderer/utils/sessionAnalyzer.test.ts +++ b/test/renderer/utils/sessionAnalyzer.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect } from 'vitest'; - import { analyzeSession } from '@renderer/utils/sessionAnalyzer'; -import type { ParsedMessage, Session, SessionDetail, SessionMetrics, Process } from '@shared/types'; +import { beforeEach, describe, expect, it } from 'vitest'; + +import type { ParsedMessage, Process, Session, SessionDetail, SessionMetrics } from '@shared/types'; // ============================================================================= // Test Helpers @@ -419,6 +419,37 @@ describe('analyzeSession', () => { const report = analyzeSession(createMockDetail({ messages })); expect(report.frictionSignals.correctionCount).toBe(0); }); + + it('does not count synthetic user replay text as friction', () => { + const messages: ParsedMessage[] = [ + createMockMessage({ + type: 'user', + isMeta: false, + isReplay: true, + isSynthetic: true, + content: 'No, wrong, actually this is synthetic replay', + }), + ]; + + const report = analyzeSession(createMockDetail({ messages })); + expect(report.frictionSignals.correctionCount).toBe(0); + expect(report.frictionSignals.frictionRate).toBe(0); + }); + + it('does not count structured protocol rows as friction', () => { + const messages: ParsedMessage[] = [ + createMockMessage({ + type: 'user', + isMeta: false, + protocolKind: 'teammate-message', + content: 'No, wrong, actually this is protocol', + }), + ]; + + const report = analyzeSession(createMockDetail({ messages })); + expect(report.frictionSignals.correctionCount).toBe(0); + expect(report.frictionSignals.frictionRate).toBe(0); + }); }); // ------------------------------------------------------------------------- @@ -562,6 +593,49 @@ describe('analyzeSession', () => { expect(report.idleAnalysis.idleGapCount).toBe(0); expect(report.idleAnalysis.totalIdleSeconds).toBe(0); }); + + it('does not treat structured protocol rows as user idle endpoints', () => { + const messages: ParsedMessage[] = [ + createMockMessage({ + type: 'assistant', + timestamp: new Date('2024-01-01T10:00:00Z'), + }), + createMockMessage({ + type: 'user', + isMeta: false, + protocolKind: 'teammate-message', + content: + 'Looks good', + timestamp: new Date('2024-01-01T10:02:00Z'), + }), + ]; + + const report = analyzeSession(createMockDetail({ messages })); + expect(report.idleAnalysis.idleGapCount).toBe(0); + expect(report.idleAnalysis.totalIdleSeconds).toBe(0); + }); + }); + + describe('key events', () => { + it('does not label structured protocol rows as user key events', () => { + const messages: ParsedMessage[] = [ + createMockMessage({ + type: 'user', + protocolKind: 'teammate-message', + content: 'start feature handoff note', + timestamp: new Date('2024-01-01T10:00:00Z'), + }), + createMockMessage({ + type: 'user', + content: 'start feature implementation', + timestamp: new Date('2024-01-01T10:01:00Z'), + }), + ]; + + const report = analyzeSession(createMockDetail({ messages })); + expect(report.keyEvents).toHaveLength(1); + expect(report.keyEvents[0]?.label).toBe('User: start feature implementation'); + }); }); // ------------------------------------------------------------------------- @@ -815,6 +889,45 @@ describe('analyzeSession', () => { expect(report.promptQuality.assessment).toBe('underspecified'); expect(report.promptQuality.firstMessageLengthChars).toBe('Fix the bug'.length); }); + + it('ignores synthetic user replay text for first prompt length', () => { + const messages: ParsedMessage[] = [ + createMockMessage({ + type: 'user', + isMeta: false, + isReplay: true, + isSynthetic: true, + content: 'Human: I tested the feature looks good', + }), + createMockMessage({ + type: 'user', + isMeta: false, + content: 'Build the real feature', + }), + ]; + + const report = analyzeSession(createMockDetail({ messages })); + expect(report.promptQuality.firstMessageLengthChars).toBe('Build the real feature'.length); + }); + + it('ignores structured protocol rows for first prompt length', () => { + const messages: ParsedMessage[] = [ + createMockMessage({ + type: 'user', + isMeta: false, + protocolKind: 'teammate-message', + content: 'plain protocol payload', + }), + createMockMessage({ + type: 'user', + isMeta: false, + content: 'Build the real feature', + }), + ]; + + const report = analyzeSession(createMockDetail({ messages })); + expect(report.promptQuality.firstMessageLengthChars).toBe('Build the real feature'.length); + }); }); describe('subagent metrics from processes', () => { diff --git a/test/shared/utils/userTurnProvenance.test.ts b/test/shared/utils/userTurnProvenance.test.ts new file mode 100644 index 00000000..58078a9f --- /dev/null +++ b/test/shared/utils/userTurnProvenance.test.ts @@ -0,0 +1,113 @@ +import { + classifyUserTurnProvenance, + isDisplayableTeammateProtocol, + isHumanAuthoredUserTurn, + isSyntheticReplayNoise, +} from '@shared/utils/userTurnProvenance'; +import { describe, expect, it } from 'vitest'; + +describe('userTurnProvenance', () => { + it('keeps replay-only user text human-authored', () => { + const message = { + type: 'user', + isReplay: true, + content: 'Human: I tested the feature looks good', + }; + + expect(classifyUserTurnProvenance(message)).toBe('human'); + expect(isHumanAuthoredUserTurn(message)).toBe(true); + }); + + it('treats an origin object without kind like absent origin', () => { + const message = { + type: 'user', + origin: {}, + content: 'ordinary user text', + }; + + expect(classifyUserTurnProvenance(message)).toBe('human'); + expect(isHumanAuthoredUserTurn(message)).toBe(true); + }); + + it('uses structured provenance before legacy text shape', () => { + const message = { + type: 'user', + protocolKind: 'teammate-message', + content: 'Plain protocol payload', + }; + + expect(classifyUserTurnProvenance(message)).toBe('teammate-protocol'); + expect(isHumanAuthoredUserTurn(message)).toBe(false); + expect(isDisplayableTeammateProtocol(message)).toBe(true); + }); + + it('keeps legacy teammate protocol detection as fallback', () => { + const message = { + type: 'user', + content: + 'Human: Looks good', + }; + + expect(classifyUserTurnProvenance(message)).toBe('teammate-protocol'); + expect(isDisplayableTeammateProtocol(message)).toBe(true); + }); + + it('hides synthetic replay text without hiding synthetic tool results or command output', () => { + expect( + isSyntheticReplayNoise({ + type: 'user', + isReplay: true, + isSynthetic: true, + content: 'Human: I tested the feature looks good', + }) + ).toBe(true); + + expect( + isSyntheticReplayNoise({ + type: 'user', + isReplay: true, + isSynthetic: true, + content: [ + { + type: 'tool_result', + tool_use_id: 'tool-1', + content: 'result', + }, + ], + }) + ).toBe(false); + + expect( + isSyntheticReplayNoise({ + type: 'user', + isReplay: true, + isSynthetic: true, + sourceToolUseID: 'tool-1', + content: 'result', + }) + ).toBe(false); + + expect( + isSyntheticReplayNoise({ + type: 'user', + isReplay: true, + isSynthetic: true, + content: 'Set model to sonnet', + }) + ).toBe(false); + }); + + it('does not display synthetic teammate protocol as real teammate output', () => { + const message = { + type: 'user', + isReplay: true, + isSynthetic: true, + protocolKind: 'teammate-message', + content: + 'Looks good', + }; + + expect(classifyUserTurnProvenance(message)).toBe('teammate-protocol'); + expect(isDisplayableTeammateProtocol(message)).toBe(false); + }); +}); From 5bb1e7bf74118ce36bc4ddc6b75ae6c7697f8088 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 00:00:37 +0300 Subject: [PATCH 31/59] ci: use packageManager pnpm version in workflows --- .github/workflows/ci.yml | 6 ------ .github/workflows/codex-runtime-smoke.yml | 2 -- .github/workflows/release.yml | 8 -------- 3 files changed, 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c53b510c..359ec075 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,8 +58,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v6 - with: - version: 10.33.0 - name: Setup Node.js uses: actions/setup-node@v6 @@ -96,8 +94,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v6 - with: - version: 10.33.0 - name: Setup Node.js uses: actions/setup-node@v6 @@ -130,8 +126,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v6 - with: - version: 10.33.0 - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/codex-runtime-smoke.yml b/.github/workflows/codex-runtime-smoke.yml index cb902999..97143b73 100644 --- a/.github/workflows/codex-runtime-smoke.yml +++ b/.github/workflows/codex-runtime-smoke.yml @@ -52,8 +52,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v6 - with: - version: 10.33.0 - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cefefe63..ba5a2abc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,8 +36,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v6 - with: - version: 10.33.0 - name: Setup Node.js uses: actions/setup-node@v6 @@ -328,8 +326,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v6 - with: - version: 10.33.0 - name: Setup Node.js uses: actions/setup-node@v6 @@ -449,8 +445,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v6 - with: - version: 10.33.0 - name: Setup Node.js uses: actions/setup-node@v6 @@ -571,8 +565,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v6 - with: - version: 10.33.0 - name: Setup Node.js uses: actions/setup-node@v6 From f2d24bbf07b657372fbafbcd4a3af3f2e505cc43 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 00:18:43 +0300 Subject: [PATCH 32/59] ci: restore electron install marker after rebuild --- package.json | 2 +- scripts/ensure-electron-install.cjs | 62 +++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 scripts/ensure-electron-install.cjs diff --git a/package.json b/package.json index 9b23c28f..f6f90065 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "standalone:build": "node --max-old-space-size=8192 ./node_modules/electron-vite/bin/electron-vite.js build && node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --config docker/vite.standalone.config.ts", "standalone:start": "node dist-standalone/index.cjs", "prepare": "husky", - "postinstall": "electron-rebuild -f -o node-pty,ssh2,cpu-features || echo 'native Electron rebuild failed (terminal/ssh features may be degraded)'" + "postinstall": "electron-rebuild -f -o node-pty,ssh2,cpu-features || echo 'native Electron rebuild failed (terminal/ssh features may be degraded)'; node ./scripts/ensure-electron-install.cjs" }, "lint-staged": { "src/**/*.{ts,tsx,js,jsx}": [ diff --git a/scripts/ensure-electron-install.cjs b/scripts/ensure-electron-install.cjs new file mode 100644 index 00000000..d91d7fbf --- /dev/null +++ b/scripts/ensure-electron-install.cjs @@ -0,0 +1,62 @@ +const childProcess = require('child_process'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +function getPlatformPath() { + const platform = process.env.npm_config_platform || os.platform(); + + switch (platform) { + case 'mas': + case 'darwin': + return 'Electron.app/Contents/MacOS/Electron'; + case 'freebsd': + case 'openbsd': + case 'linux': + return 'electron'; + case 'win32': + return 'electron.exe'; + default: + throw new Error(`Electron builds are not available on platform: ${platform}`); + } +} + +function ensurePathFile(electronDir, platformPath) { + const pathFile = path.join(electronDir, 'path.txt'); + const distPath = process.env.ELECTRON_OVERRIDE_DIST_PATH || path.join(electronDir, 'dist'); + const executablePath = path.join(distPath, platformPath); + + if (!fs.existsSync(executablePath)) { + return false; + } + + const currentPath = fs.existsSync(pathFile) ? fs.readFileSync(pathFile, 'utf8') : ''; + if (currentPath !== platformPath) { + fs.writeFileSync(pathFile, platformPath); + } + return true; +} + +function runElectronInstaller(installPath) { + const result = childProcess.spawnSync(process.execPath, [installPath], { + stdio: 'inherit', + env: process.env, + }); + + if (result.status !== 0) { + throw new Error(`Electron installer failed with exit code ${result.status ?? 'unknown'}`); + } +} + +const electronPackagePath = require.resolve('electron/package.json'); +const electronDir = path.dirname(electronPackagePath); +const installPath = path.join(electronDir, 'install.js'); +const platformPath = getPlatformPath(); + +if (!ensurePathFile(electronDir, platformPath)) { + runElectronInstaller(installPath); +} + +if (!ensurePathFile(electronDir, platformPath)) { + throw new Error(`Electron binary is missing after install: ${platformPath}`); +} From 444ed7e6400bfa2c4595b28e6d8482b9eebfc9d0 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 00:24:49 +0300 Subject: [PATCH 33/59] ci: tolerate missing electron binary marker --- scripts/ensure-electron-install.cjs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/scripts/ensure-electron-install.cjs b/scripts/ensure-electron-install.cjs index d91d7fbf..af85c37a 100644 --- a/scripts/ensure-electron-install.cjs +++ b/scripts/ensure-electron-install.cjs @@ -21,20 +21,21 @@ function getPlatformPath() { } } -function ensurePathFile(electronDir, platformPath) { +function getElectronPaths(electronDir, platformPath) { const pathFile = path.join(electronDir, 'path.txt'); const distPath = process.env.ELECTRON_OVERRIDE_DIST_PATH || path.join(electronDir, 'dist'); const executablePath = path.join(distPath, platformPath); - if (!fs.existsSync(executablePath)) { - return false; - } + return { executablePath, pathFile }; +} + +function ensurePathFile(electronDir, platformPath) { + const { pathFile } = getElectronPaths(electronDir, platformPath); const currentPath = fs.existsSync(pathFile) ? fs.readFileSync(pathFile, 'utf8') : ''; if (currentPath !== platformPath) { fs.writeFileSync(pathFile, platformPath); } - return true; } function runElectronInstaller(installPath) { @@ -52,11 +53,15 @@ const electronPackagePath = require.resolve('electron/package.json'); const electronDir = path.dirname(electronPackagePath); const installPath = path.join(electronDir, 'install.js'); const platformPath = getPlatformPath(); +const { executablePath, pathFile } = getElectronPaths(electronDir, platformPath); -if (!ensurePathFile(electronDir, platformPath)) { +if (!fs.existsSync(executablePath)) { runElectronInstaller(installPath); } -if (!ensurePathFile(electronDir, platformPath)) { - throw new Error(`Electron binary is missing after install: ${platformPath}`); +ensurePathFile(electronDir, platformPath); + +if (!fs.existsSync(executablePath)) { + console.warn(`Electron binary is missing after install: ${executablePath}`); + console.warn(`Wrote Electron import marker: ${pathFile}`); } From abce029ef6a6dd3d151b0cf4259a733e880a9859 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 00:54:25 +0300 Subject: [PATCH 34/59] ci: reduce lead relay state regex complexity --- .../services/team/TeamProvisioningService.ts | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index fb0b1c9f..1199b685 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -3153,6 +3153,45 @@ function normalizeSameTeamText(text: string): string { return text.trim().replace(/\r\n/g, '\n'); } +const SUPPRESSED_LEAD_RELAY_STATE_PHRASES = [ + 'open', + 'closed', + 'merged', + 'approved', + 'complete', + 'completed', + 'done', + 'blocked', + 'pending', + 'in_progress', + 'in progress', + 'needsfix', + 'needs fix', + 'in review', + 'clear', +] as const; + +function startsWithSuppressedLeadRelayStatePhrase(text: string): boolean { + const lowerText = text.toLowerCase(); + return SUPPRESSED_LEAD_RELAY_STATE_PHRASES.some((phrase) => { + if (!lowerText.startsWith(phrase)) { + return false; + } + + const nextChar = lowerText.charAt(phrase.length); + return nextChar.length === 0 || !/[a-z0-9_]/i.test(nextChar); + }); +} + +function hasSuppressedLeadRelayStatePredicate(normalized: string): boolean { + const match = /\b(?:is|are|was|were|stays?|still|now)\s+/i.exec(normalized); + if (!match) { + return false; + } + + return startsWithSuppressedLeadRelayStatePhrase(normalized.slice(match.index + match[0].length)); +} + function shouldSuppressUnverifiedLeadRelayStateLine(text: string): boolean { const normalized = text.trim().replace(/\s+/g, ' '); if (normalized.length === 0) { @@ -3175,9 +3214,7 @@ function shouldSuppressUnverifiedLeadRelayStateLine(text: string): boolean { /\b(?:done|complete(?:d)?|approved|merged|closed|blocked|resolved|failed|succeeded)\b/i.test( normalized ) || - /\b(?:is|are|was|were|stays?|still|now)\s+(?:open|closed|merged|approved|complete(?:d)?|done|blocked|pending|in_progress|in progress|needsfix|needs fix|in review|clear)\b/i.test( - normalized - ) || + hasSuppressedLeadRelayStatePredicate(normalized) || /\b(?:mergecommit|mergedat)\s*=\s*(?:null|[^\s,;]+)/i.test(normalized) || /\bqueue\b.*\bclear\b/i.test(normalized) ); From 9d5f17659731fc8bcac9464e11c75d4a8df2f853 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 01:07:07 +0300 Subject: [PATCH 35/59] test: stabilize runtime provider management assertion --- .../useRuntimeProviderManagement.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts b/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts index 82c5f774..1948bb0e 100644 --- a/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts +++ b/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts @@ -231,7 +231,7 @@ describe('useRuntimeProviderManagement', () => { }); await vi.waitFor(() => { - expect(state?.error).toContain('wrong runtime binary'); + expect(state?.error ?? '').toContain('wrong runtime binary'); }); expect(state?.errorDiagnostics?.binaryPath).toBe('/opt/homebrew/bin/opencode'); From ebcc0e717f057c110013bcc1f53ce45a4393ba47 Mon Sep 17 00:00:00 2001 From: infiniti <52129260+developerInfiniti@users.noreply.github.com> Date: Wed, 27 May 2026 12:16:41 +0300 Subject: [PATCH 36/59] fix(team): reconcile provisioned-but-not-alive bootstrap state --- .../renderer/adapters/TeamGraphAdapter.ts | 42 +- .../renderer/ui/GraphNodePopover.tsx | 4 + .../buildMixedPersistedLaunchSnapshot.test.ts | 207 +++++ .../buildMixedPersistedLaunchSnapshot.ts | 84 +- .../team/TeamLaunchSummaryProjection.ts | 227 ++++- .../services/team/TeamProvisioningService.ts | 207 ++++- .../TeamProvisioningLaunchDiagnostics.ts | 31 +- .../TeamProvisioningLaunchFailurePolicy.ts | 30 +- .../TeamProvisioningPromptBuilders.ts | 72 +- .../components/team/members/MemberCard.tsx | 39 +- .../team/members/MemberDetailDialog.tsx | 18 +- .../team/members/MemberDetailHeader.tsx | 13 + .../team/members/MemberHoverCard.tsx | 6 + .../components/team/members/MemberList.tsx | 58 +- .../components/team/provisioningSteps.ts | 73 +- .../components/team/teamRuntimeDisplayRows.ts | 60 +- src/renderer/store/slices/teamSlice.ts | 21 + src/renderer/utils/memberHelpers.ts | 223 +++-- src/renderer/utils/memberLaunchDiagnostics.ts | 129 ++- .../utils/teamProvisioningPresentation.ts | 24 +- src/shared/utils/teamLaunchFailureReason.ts | 142 +++ .../services/team/TeamConfigReader.test.ts | 148 ++- .../team/TeamLaunchSummaryProjection.test.ts | 422 +++++++++ .../TeamProvisioningLaunchDiagnostics.test.ts | 135 ++- ...eamProvisioningLaunchFailurePolicy.test.ts | 98 +- .../TeamProvisioningPromptBuilders.test.ts | 59 ++ .../team/TeamProvisioningService.test.ts | 870 +++++++++++++++--- .../team/members/MemberCard.test.ts | 49 + .../team/members/MemberDetailDialog.test.ts | 61 ++ .../team/members/MemberList.test.ts | 156 +++- .../components/team/provisioningSteps.test.ts | 329 +++++++ .../team/teamRuntimeDisplayRows.test.ts | 272 +++++- .../agent-graph/TeamGraphAdapter.test.ts | 148 +++ test/renderer/store/teamSlice.test.ts | 90 ++ test/renderer/utils/memberHelpers.test.ts | 360 +++++++- .../utils/memberLaunchDiagnostics.test.ts | 355 +++++++ .../teamProvisioningPresentation.test.ts | 121 +++ .../utils/teamLaunchFailureReason.test.ts | 149 +++ 38 files changed, 5114 insertions(+), 418 deletions(-) create mode 100644 src/shared/utils/teamLaunchFailureReason.ts create mode 100644 test/main/services/team/TeamProvisioningPromptBuilders.test.ts create mode 100644 test/shared/utils/teamLaunchFailureReason.test.ts diff --git a/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts index 25d11a8b..ce88d46c 100644 --- a/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts +++ b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts @@ -38,6 +38,10 @@ import { import { isInboxNoiseMessage } from '@shared/utils/inboxNoise'; import { isLeadMember } from '@shared/utils/leadDetection'; import { buildOrderedVisibleTeamGraphOwnerIds } from '@shared/utils/teamGraphDefaultLayout'; +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext, + isBootstrapConfirmedProvisionedButNotAliveFailure, +} from '@shared/utils/teamLaunchFailureReason'; import { isTeamTaskActivelyWorked, isTeamTaskNeedsFixActionable, @@ -560,6 +564,7 @@ export class TeamGraphAdapter { member.runtimeAdvisory, member.providerId, spawn, + runtimeEntry, pendingApprovalAgents?.has(member.name) ?? false ); const currentTask = member.currentTaskId @@ -581,7 +586,11 @@ export class TeamGraphAdapter { spawnBootstrapStalled: spawn?.bootstrapStalled, spawnAgentToolAccepted: spawn?.agentToolAccepted, spawnHardFailure: spawn?.hardFailure, + spawnHardFailureReason: spawn?.hardFailureReason, + spawnError: spawn?.error, + spawnRuntimeDiagnostic: spawn?.runtimeDiagnostic, spawnLivenessKind: spawn?.livenessKind, + spawnRuntimeDiagnosticSeverity: spawn?.runtimeDiagnosticSeverity, spawnFirstSpawnAcceptedAt: spawn?.firstSpawnAcceptedAt, spawnUpdatedAt: spawn?.updatedAt, runtimeEntry, @@ -599,7 +608,7 @@ export class TeamGraphAdapter { ? 'terminated' : hasRunningTool ? 'tool_calling' - : TeamGraphAdapter.#mapMemberStatus(member.status, spawn), + : TeamGraphAdapter.#mapMemberStatus(member.status, spawn, runtimeEntry), color: isTeamVisualOnline ? (member.color ?? undefined) : undefined, role: member.role ?? undefined, runtimeLabel: TeamGraphAdapter.#getRuntimeLabel( @@ -1269,9 +1278,15 @@ export class TeamGraphAdapter { runtimeAdvisory: ResolvedTeamMember['runtimeAdvisory'], providerId: ResolvedTeamMember['providerId'], spawn: MemberSpawnStatusEntry | undefined, + runtimeEntry: TeamAgentRuntimeEntry | undefined, pendingApproval: boolean ): Pick | undefined { - if (spawn?.launchState === 'failed_to_start' || spawn?.status === 'error') { + const hasUnsuppressedSpawnFailure = + TeamGraphAdapter.#hasUnsuppressedProvisionedButNotAliveFailure(spawn, runtimeEntry); + if ( + hasUnsuppressedSpawnFailure && + (spawn?.launchState === 'failed_to_start' || spawn?.status === 'error') + ) { return { exceptionTone: 'error', exceptionLabel: 'spawn failed' }; } if (pendingApproval || spawn?.launchState === 'runtime_pending_permission') { @@ -1290,10 +1305,19 @@ export class TeamGraphAdapter { return undefined; } - static #mapMemberStatus(status: string, spawn?: MemberSpawnStatusEntry): GraphNodeState { + static #mapMemberStatus( + status: string, + spawn?: MemberSpawnStatusEntry, + runtimeEntry?: TeamAgentRuntimeEntry + ): GraphNodeState { if (spawn?.launchState === 'runtime_pending_permission') return 'waiting'; if (spawn?.status === 'spawning') return 'thinking'; - if (spawn?.status === 'error') return 'error'; + if ( + spawn?.status === 'error' && + TeamGraphAdapter.#hasUnsuppressedProvisionedButNotAliveFailure(spawn, runtimeEntry) + ) { + return 'error'; + } if (spawn?.status === 'waiting') return 'waiting'; switch (status) { case 'active': @@ -1307,6 +1331,16 @@ export class TeamGraphAdapter { } } + static #hasUnsuppressedProvisionedButNotAliveFailure( + spawn: MemberSpawnStatusEntry | undefined, + runtimeEntry: TeamAgentRuntimeEntry | undefined + ): boolean { + return ( + !isBootstrapConfirmedProvisionedButNotAliveFailure(spawn) || + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawn, runtimeEntry) + ); + } + static #mapTaskStatus(status: string): GraphNodeState { switch (status) { case 'pending': diff --git a/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx b/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx index 6ea2258d..3bdce9f2 100644 --- a/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx +++ b/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx @@ -373,7 +373,11 @@ const MemberPopoverContent = ({ spawnBootstrapStalled: spawnEntry?.bootstrapStalled, spawnAgentToolAccepted: spawnEntry?.agentToolAccepted, spawnHardFailure: spawnEntry?.hardFailure, + spawnHardFailureReason: spawnEntry?.hardFailureReason, + spawnError: spawnEntry?.error, + spawnRuntimeDiagnostic: spawnEntry?.runtimeDiagnostic, spawnLivenessKind: spawnEntry?.livenessKind, + spawnRuntimeDiagnosticSeverity: spawnEntry?.runtimeDiagnosticSeverity, spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt, spawnUpdatedAt: spawnEntry?.updatedAt, runtimeEntry, diff --git a/src/features/team-runtime-lanes/core/domain/__tests__/buildMixedPersistedLaunchSnapshot.test.ts b/src/features/team-runtime-lanes/core/domain/__tests__/buildMixedPersistedLaunchSnapshot.test.ts index dc6fb0f7..d7cd1d51 100644 --- a/src/features/team-runtime-lanes/core/domain/__tests__/buildMixedPersistedLaunchSnapshot.test.ts +++ b/src/features/team-runtime-lanes/core/domain/__tests__/buildMixedPersistedLaunchSnapshot.test.ts @@ -258,6 +258,213 @@ describe('buildMixedPersistedLaunchSnapshot', () => { expect(snapshot.teamLaunchState).toBe('partial_failure'); }); + it('heals bootstrap-confirmed provisioned-but-not-alive primary status while building snapshots', () => { + const snapshot = buildMixedPersistedLaunchSnapshot({ + teamName: 'signal-ops', + launchPhase: 'finished', + updatedAt: '2026-05-25T20:14:02.147Z', + leadDefaults: { + providerId: 'anthropic', + providerBackendId: null, + selectedFastMode: 'off', + resolvedFastMode: false, + launchIdentity: null, + }, + primaryMembers: [{ name: 'tom', providerId: 'anthropic', model: 'sonnet', effort: 'medium' }], + primaryStatuses: { + tom: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + updatedAt: '2026-05-25T20:14:02.147Z', + } as never, + }, + secondaryMembers: [], + }); + + expect(snapshot.members.tom).toMatchObject({ + launchState: 'confirmed_alive', + bootstrapConfirmed: true, + runtimeAlive: true, + hardFailure: false, + hardFailureReason: undefined, + livenessKind: 'confirmed_bootstrap', + }); + expect(snapshot.summary).toMatchObject({ + confirmedCount: 1, + failedCount: 0, + pendingCount: 0, + }); + expect(snapshot.teamLaunchState).toBe('clean_success'); + }); + + it('heals Windows process-table-unavailable provisioned-but-not-alive primary metadata', () => { + const snapshot = buildMixedPersistedLaunchSnapshot({ + teamName: 'signal-ops', + launchPhase: 'finished', + updatedAt: '2026-05-25T20:14:02.147Z', + leadDefaults: { + providerId: 'anthropic', + providerBackendId: null, + selectedFastMode: 'off', + resolvedFastMode: false, + launchIdentity: null, + }, + primaryMembers: [{ name: 'tom', providerId: 'anthropic', model: 'sonnet', effort: 'medium' }], + primaryStatuses: { + tom: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + livenessKind: 'stale_metadata', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + updatedAt: '2026-05-25T20:14:02.147Z', + } as never, + }, + secondaryMembers: [], + }); + + expect(snapshot.members.tom).toMatchObject({ + launchState: 'confirmed_alive', + bootstrapConfirmed: true, + runtimeAlive: true, + hardFailure: false, + }); + expect(snapshot.summary).toMatchObject({ + confirmedCount: 1, + failedCount: 0, + }); + }); + + it('keeps bootstrap-confirmed provisioned-but-not-alive primary status failed when diagnostics are errors', () => { + const snapshot = buildMixedPersistedLaunchSnapshot({ + teamName: 'signal-ops', + launchPhase: 'finished', + updatedAt: '2026-05-25T20:14:02.147Z', + leadDefaults: { + providerId: 'anthropic', + providerBackendId: null, + selectedFastMode: 'off', + resolvedFastMode: false, + launchIdentity: null, + }, + primaryMembers: [{ name: 'tom', providerId: 'anthropic', model: 'sonnet', effort: 'medium' }], + primaryStatuses: { + tom: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + updatedAt: '2026-05-25T20:14:02.147Z', + } as never, + }, + secondaryMembers: [], + }); + + expect(snapshot.members.tom).toMatchObject({ + launchState: 'failed_to_start', + bootstrapConfirmed: true, + runtimeAlive: false, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }); + expect(snapshot.summary).toMatchObject({ + confirmedCount: 0, + failedCount: 1, + pendingCount: 0, + }); + expect(snapshot.teamLaunchState).toBe('partial_failure'); + }); + + it('keeps bootstrap-confirmed provisioned-but-not-alive secondary status failed when liveness is stopped', () => { + const snapshot = buildMixedPersistedLaunchSnapshot({ + teamName: 'mixed-team', + launchPhase: 'finished', + updatedAt: '2026-05-25T20:14:02.147Z', + leadDefaults: { + providerId: 'codex', + providerBackendId: 'codex-native', + selectedFastMode: 'off', + resolvedFastMode: false, + launchIdentity: null, + }, + primaryMembers: [], + primaryStatuses: {}, + secondaryMembers: [ + { + laneId: 'secondary:opencode:tom', + member: { + name: 'tom', + providerId: 'opencode', + model: 'minimax-m2.5-free', + effort: 'medium', + }, + leadDefaults: { + providerId: 'codex', + providerBackendId: 'codex-native', + selectedFastMode: 'off', + resolvedFastMode: false, + launchIdentity: null, + }, + evidence: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + } as never, + }, + ], + }); + + expect(snapshot.members.tom).toMatchObject({ + laneKind: 'secondary', + launchState: 'failed_to_start', + runtimeAlive: false, + hardFailure: true, + livenessKind: 'not_found', + runtimeDiagnosticSeverity: 'warning', + }); + expect(snapshot.summary).toMatchObject({ + confirmedCount: 0, + failedCount: 1, + pendingCount: 0, + }); + expect(snapshot.teamLaunchState).toBe('partial_failure'); + }); + it('preserves permission-blocked side-lane members as runtime_pending_permission', () => { const snapshot = buildMixedPersistedLaunchSnapshot({ teamName: 'mixed-team', diff --git a/src/features/team-runtime-lanes/core/domain/buildMixedPersistedLaunchSnapshot.ts b/src/features/team-runtime-lanes/core/domain/buildMixedPersistedLaunchSnapshot.ts index 5eae2e40..11568134 100644 --- a/src/features/team-runtime-lanes/core/domain/buildMixedPersistedLaunchSnapshot.ts +++ b/src/features/team-runtime-lanes/core/domain/buildMixedPersistedLaunchSnapshot.ts @@ -1,10 +1,13 @@ import { isLeadMember } from '@shared/utils/leadDetection'; import { migrateProviderBackendId } from '@shared/utils/providerBackend'; +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidence, + isBootstrapConfirmedProvisionedButNotAliveFailure, +} from '@shared/utils/teamLaunchFailureReason'; import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider'; import type { MemberLaunchState, - MemberSpawnLivenessSource, MemberSpawnStatusEntry, OpenCodeAppManagedBootstrapCandidate, OpenCodeBootstrapEvidenceSource, @@ -95,6 +98,20 @@ function preservesStrongRuntimeAlive(value: { ); } +function canHealBootstrapConfirmedProvisionedButNotAliveFailure( + entry: + | (Parameters[0] & { + runtimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity; + livenessKind?: TeamAgentRuntimeLivenessKind; + }) + | undefined +): boolean { + return ( + isBootstrapConfirmedProvisionedButNotAliveFailure(entry) && + !hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry) + ); +} + function hasMaterializedOpenCodeRuntimeMarker(value: { runtimeAlive?: boolean; runtimePid?: number; @@ -233,16 +250,22 @@ function createPrimaryLaneMemberState(params: { const runtime = params.status; const strongRuntimeAlive = preservesStrongRuntimeAlive(runtime ?? {}); const sources = runtime ? createSourcesFromStatus(runtime) : undefined; - const launchState = - runtime?.launchState ?? - deriveMemberLaunchState({ - hardFailure: runtime?.hardFailure, - bootstrapConfirmed: runtime?.bootstrapConfirmed, - runtimeAlive: strongRuntimeAlive, - agentToolAccepted: runtime?.agentToolAccepted, - pendingPermissionRequestIds: runtime?.pendingPermissionRequestIds, - }); - const hardFailure = runtime?.hardFailure === true || launchState === 'failed_to_start'; + const healBootstrapConfirmedProvisionedButNotAlive = + canHealBootstrapConfirmedProvisionedButNotAliveFailure(runtime); + const runtimeAlive = healBootstrapConfirmedProvisionedButNotAlive || strongRuntimeAlive; + const launchState = healBootstrapConfirmedProvisionedButNotAlive + ? 'confirmed_alive' + : (runtime?.launchState ?? + deriveMemberLaunchState({ + hardFailure: runtime?.hardFailure, + bootstrapConfirmed: runtime?.bootstrapConfirmed, + runtimeAlive: strongRuntimeAlive, + agentToolAccepted: runtime?.agentToolAccepted, + pendingPermissionRequestIds: runtime?.pendingPermissionRequestIds, + })); + const hardFailure = + !healBootstrapConfirmedProvisionedButNotAlive && + (runtime?.hardFailure === true || launchState === 'failed_to_start'); const base: PersistedTeamLaunchMemberState = { name: params.member.name.trim(), providerId, @@ -272,7 +295,7 @@ function createPrimaryLaneMemberState(params: { : undefined, launchState, agentToolAccepted: runtime?.agentToolAccepted === true, - runtimeAlive: strongRuntimeAlive, + runtimeAlive, bootstrapConfirmed: runtime?.bootstrapConfirmed === true, hardFailure, hardFailureReason: hardFailure ? (runtime?.hardFailureReason ?? runtime?.error) : undefined, @@ -285,7 +308,7 @@ function createPrimaryLaneMemberState(params: { firstSpawnAcceptedAt: runtime?.firstSpawnAcceptedAt, lastHeartbeatAt: runtime?.lastHeartbeatAt, runtimeLastSeenAt: runtime?.livenessLastCheckedAt, - lastRuntimeAliveAt: preservesStrongRuntimeAlive(runtime ?? {}) ? params.updatedAt : undefined, + lastRuntimeAliveAt: runtimeAlive ? params.updatedAt : undefined, lastEvaluatedAt: runtime?.updatedAt ?? params.updatedAt, sources, diagnostics: undefined, @@ -301,16 +324,22 @@ function createSecondaryLaneMemberState( normalizeOptionalTeamProviderId(params.member.providerId) ?? params.leadDefaults.providerId; const evidence = params.evidence; const strongRuntimeAlive = preservesStrongRuntimeAlive(evidence ?? {}); - const launchState = - evidence?.launchState ?? - deriveMemberLaunchState({ - hardFailure: evidence?.hardFailure, - bootstrapConfirmed: evidence?.bootstrapConfirmed, - runtimeAlive: strongRuntimeAlive, - agentToolAccepted: evidence?.agentToolAccepted, - pendingPermissionRequestIds: evidence?.pendingPermissionRequestIds, - }); - const hardFailure = evidence?.hardFailure === true || launchState === 'failed_to_start'; + const healBootstrapConfirmedProvisionedButNotAlive = + canHealBootstrapConfirmedProvisionedButNotAliveFailure(evidence ?? undefined); + const runtimeAlive = healBootstrapConfirmedProvisionedButNotAlive || strongRuntimeAlive; + const launchState = healBootstrapConfirmedProvisionedButNotAlive + ? 'confirmed_alive' + : (evidence?.launchState ?? + deriveMemberLaunchState({ + hardFailure: evidence?.hardFailure, + bootstrapConfirmed: evidence?.bootstrapConfirmed, + runtimeAlive: strongRuntimeAlive, + agentToolAccepted: evidence?.agentToolAccepted, + pendingPermissionRequestIds: evidence?.pendingPermissionRequestIds, + })); + const hardFailure = + !healBootstrapConfirmedProvisionedButNotAlive && + (evidence?.hardFailure === true || launchState === 'failed_to_start'); const hardFailureReason = hardFailure ? evidence?.hardFailureReason : undefined; const firstSpawnAcceptedAt = evidence ? resolveOpenCodeSecondaryFirstSpawnAcceptedAt(evidence, params.updatedAt) @@ -340,7 +369,7 @@ function createSecondaryLaneMemberState( laneOwnerProviderId: providerId, launchState, agentToolAccepted: evidence?.agentToolAccepted === true, - runtimeAlive: strongRuntimeAlive, + runtimeAlive, bootstrapConfirmed: evidence?.bootstrapConfirmed === true, hardFailure, hardFailureReason, @@ -373,7 +402,7 @@ function createSecondaryLaneMemberState( firstSpawnAcceptedAt, lastHeartbeatAt: evidence?.bootstrapConfirmed ? params.updatedAt : undefined, runtimeLastSeenAt: strongRuntimeAlive ? params.updatedAt : undefined, - lastRuntimeAliveAt: strongRuntimeAlive ? params.updatedAt : undefined, + lastRuntimeAliveAt: runtimeAlive ? params.updatedAt : undefined, lastEvaluatedAt: params.updatedAt, sources: strongRuntimeAlive ? { @@ -412,7 +441,10 @@ function summarizeMembers( pendingCount += 1; continue; } - if (entry.launchState === 'confirmed_alive') { + if ( + entry.launchState === 'confirmed_alive' || + canHealBootstrapConfirmedProvisionedButNotAliveFailure(entry) + ) { confirmedCount += 1; continue; } diff --git a/src/main/services/team/TeamLaunchSummaryProjection.ts b/src/main/services/team/TeamLaunchSummaryProjection.ts index ac29deef..3dd6befb 100644 --- a/src/main/services/team/TeamLaunchSummaryProjection.ts +++ b/src/main/services/team/TeamLaunchSummaryProjection.ts @@ -1,10 +1,26 @@ import { isMixedOpenCodeSideLanePlan, planTeamRuntimeLanes } from '@features/team-runtime-lanes'; +import { + hasBootstrapConfirmationProofForLaunchFailure, + hasUnsafeProvisionedButNotAliveRuntimeEvidence, + isProvisionedButNotAliveLaunchFailure, +} from '@shared/utils/teamLaunchFailureReason'; import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider'; +import { isBootstrapMemberEvidenceCurrentForMember } from './provisioning/TeamProvisioningOpenCodeRuntimeEvidencePolicy'; import { shouldIgnoreTerminalBootstrapOnlyPendingSnapshot } from './TeamBootstrapStateReader'; -import { hasMixedPersistedLaunchMetadata } from './TeamLaunchStateEvaluator'; +import { + deriveTeamLaunchAggregateState, + hasMixedPersistedLaunchMetadata, + summarizePersistedLaunchMembers, +} from './TeamLaunchStateEvaluator'; -import type { PersistedTeamLaunchSnapshot, TeamProviderId, TeamSummary } from '@shared/types'; +import type { + PersistedTeamLaunchMemberState, + PersistedTeamLaunchSnapshot, + PersistedTeamLaunchSummary, + TeamProviderId, + TeamSummary, +} from '@shared/types'; export const TEAM_LAUNCH_SUMMARY_FILE = 'launch-summary.json'; const STALE_PENDING_SUMMARY_GRACE_MS = 5 * 60 * 1000; @@ -41,6 +57,71 @@ function getPersistedLaunchMemberNames(snapshot: PersistedTeamLaunchSnapshot): s return Array.from(new Set([...snapshot.expectedMembers, ...Object.keys(snapshot.members)])); } +function hasBootstrapConfirmationProof( + member: PersistedTeamLaunchMemberState, + bootstrapMember: PersistedTeamLaunchMemberState | undefined +): boolean { + if (hasBootstrapConfirmationProofForLaunchFailure(member)) { + return true; + } + return ( + bootstrapMember != null && + hasBootstrapConfirmationProofForLaunchFailure(bootstrapMember) && + isBootstrapMemberEvidenceCurrentForMember(member, bootstrapMember, 'confirmation') + ); +} + +function shouldProjectProvisionedButNotAliveAsConfirmed(params: { + member: PersistedTeamLaunchMemberState | undefined; + bootstrapMember?: PersistedTeamLaunchMemberState; +}): params is { member: PersistedTeamLaunchMemberState } { + const member = params.member; + if (member?.launchState !== 'failed_to_start' || member.hardFailure !== true) { + return false; + } + if ( + hasUnsafeProvisionedButNotAliveRuntimeEvidence(member) || + hasUnsafeProvisionedButNotAliveRuntimeEvidence(params.bootstrapMember) + ) { + return false; + } + return ( + isProvisionedButNotAliveLaunchFailure(member) && + hasBootstrapConfirmationProof(member, params.bootstrapMember) + ); +} + +function buildProjectedMembersForSummary( + snapshot: PersistedTeamLaunchSnapshot, + bootstrapSnapshot?: PersistedTeamLaunchSnapshot | null +): Record | null { + let changed = false; + const projectedMembers: Record = {}; + for (const [memberName, member] of Object.entries(snapshot.members)) { + if ( + shouldProjectProvisionedButNotAliveAsConfirmed({ + member, + bootstrapMember: bootstrapSnapshot?.members[memberName], + }) + ) { + changed = true; + projectedMembers[memberName] = { + ...member, + launchState: 'confirmed_alive', + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + hardFailureReason: undefined, + runtimeDiagnostic: undefined, + runtimeDiagnosticSeverity: undefined, + }; + continue; + } + projectedMembers[memberName] = member; + } + return changed ? projectedMembers : null; +} + function normalizeIsoDate(value: unknown): string | null { if (typeof value !== 'string') { return null; @@ -57,42 +138,47 @@ function toMillis(value: string | undefined | null): number { } export function createLaunchStateSummary( - snapshot: PersistedTeamLaunchSnapshot + snapshot: PersistedTeamLaunchSnapshot, + options: { bootstrapSnapshot?: PersistedTeamLaunchSnapshot | null } = {} ): LaunchStateSummary { const persistedMemberNames = getPersistedLaunchMemberNames(snapshot); + const projectedMembers = buildProjectedMembersForSummary(snapshot, options.bootstrapSnapshot); + const members = projectedMembers ?? snapshot.members; + const summary = projectedMembers + ? summarizePersistedLaunchMembers(snapshot.expectedMembers, projectedMembers) + : snapshot.summary; + const teamLaunchState = projectedMembers + ? deriveTeamLaunchAggregateState(summary) + : snapshot.teamLaunchState; const missingMembers = persistedMemberNames.filter((name) => { - const member = snapshot.members[name]; + const member = members[name]; return member?.launchState === 'failed_to_start'; }); const skippedMembers = persistedMemberNames.filter((name) => { - const member = snapshot.members[name]; + const member = members[name]; return member?.launchState === 'skipped_for_launch' || member?.skippedForLaunch === true; }); return { - ...(snapshot.teamLaunchState === 'partial_failure' - ? { partialLaunchFailure: true as const } - : {}), + ...(teamLaunchState === 'partial_failure' ? { partialLaunchFailure: true as const } : {}), ...(persistedMemberNames.length > 0 ? { expectedMemberCount: persistedMemberNames.length } : {}), - ...(snapshot.summary.confirmedCount > 0 - ? { confirmedMemberCount: snapshot.summary.confirmedCount } - : {}), + ...(summary.confirmedCount > 0 ? { confirmedMemberCount: summary.confirmedCount } : {}), ...(missingMembers.length > 0 ? { missingMembers } : {}), ...(skippedMembers.length > 0 ? { skippedMembers } : {}), - teamLaunchState: snapshot.teamLaunchState, + teamLaunchState, launchUpdatedAt: snapshot.updatedAt, - confirmedCount: snapshot.summary.confirmedCount, - pendingCount: snapshot.summary.pendingCount, - failedCount: snapshot.summary.failedCount, - skippedCount: snapshot.summary.skippedCount, - runtimeAlivePendingCount: snapshot.summary.runtimeAlivePendingCount, - shellOnlyPendingCount: snapshot.summary.shellOnlyPendingCount, - runtimeProcessPendingCount: snapshot.summary.runtimeProcessPendingCount, - runtimeCandidatePendingCount: snapshot.summary.runtimeCandidatePendingCount, - noRuntimePendingCount: snapshot.summary.noRuntimePendingCount, - permissionPendingCount: snapshot.summary.permissionPendingCount, + confirmedCount: summary.confirmedCount, + pendingCount: summary.pendingCount, + failedCount: summary.failedCount, + skippedCount: summary.skippedCount, + runtimeAlivePendingCount: summary.runtimeAlivePendingCount, + shellOnlyPendingCount: summary.shellOnlyPendingCount, + runtimeProcessPendingCount: summary.runtimeProcessPendingCount, + runtimeCandidatePendingCount: summary.runtimeCandidatePendingCount, + noRuntimePendingCount: summary.noRuntimePendingCount, + permissionPendingCount: summary.permissionPendingCount, }; } @@ -242,6 +328,83 @@ function shouldIgnoreStalePendingLaunchSnapshotSummary( return Number.isFinite(updatedAtMs) && nowMs - updatedAtMs >= STALE_PENDING_SUMMARY_GRACE_MS; } +function reconcileSummaryProjectionWithBootstrap( + projection: PersistedTeamLaunchSummaryProjection, + bootstrapSnapshot: PersistedTeamLaunchSnapshot +): PersistedTeamLaunchSummaryProjection { + const missingMembers = projection.missingMembers ?? []; + if (missingMembers.length === 0) { + return projection; + } + + const projectionBoundary = projection.launchUpdatedAt ?? projection.updatedAt; + const healedMembers = missingMembers.filter((memberName) => { + const bootstrapMember = bootstrapSnapshot.members[memberName]; + return ( + bootstrapMember != null && + hasBootstrapConfirmationProofForLaunchFailure(bootstrapMember) && + !hasUnsafeProvisionedButNotAliveRuntimeEvidence(bootstrapMember) && + isBootstrapMemberEvidenceCurrentForMember( + { firstSpawnAcceptedAt: projectionBoundary, lastEvaluatedAt: projectionBoundary }, + bootstrapMember, + 'confirmation' + ) + ); + }); + if (healedMembers.length === 0) { + return projection; + } + + const healedMemberNames = new Set(healedMembers); + const nextMissingMembers = missingMembers.filter( + (memberName) => !healedMemberNames.has(memberName) + ); + const summary: PersistedTeamLaunchSummary = { + confirmedCount: + (projection.confirmedCount ?? projection.confirmedMemberCount ?? 0) + healedMembers.length, + pendingCount: projection.pendingCount ?? 0, + failedCount: Math.max( + 0, + (projection.failedCount ?? missingMembers.length) - healedMembers.length + ), + skippedCount: projection.skippedCount ?? projection.skippedMembers?.length ?? 0, + runtimeAlivePendingCount: projection.runtimeAlivePendingCount ?? 0, + shellOnlyPendingCount: projection.shellOnlyPendingCount, + runtimeProcessPendingCount: projection.runtimeProcessPendingCount, + runtimeCandidatePendingCount: projection.runtimeCandidatePendingCount, + noRuntimePendingCount: projection.noRuntimePendingCount, + permissionPendingCount: projection.permissionPendingCount, + }; + const teamLaunchState = deriveTeamLaunchAggregateState(summary); + + const reconciled: PersistedTeamLaunchSummaryProjection = { + ...projection, + teamLaunchState, + confirmedMemberCount: summary.confirmedCount, + confirmedCount: summary.confirmedCount, + pendingCount: summary.pendingCount, + failedCount: summary.failedCount, + skippedCount: summary.skippedCount, + runtimeAlivePendingCount: summary.runtimeAlivePendingCount, + shellOnlyPendingCount: summary.shellOnlyPendingCount, + runtimeProcessPendingCount: summary.runtimeProcessPendingCount, + runtimeCandidatePendingCount: summary.runtimeCandidatePendingCount, + noRuntimePendingCount: summary.noRuntimePendingCount, + permissionPendingCount: summary.permissionPendingCount, + }; + if (nextMissingMembers.length > 0) { + reconciled.missingMembers = nextMissingMembers; + } else { + delete reconciled.missingMembers; + } + if (teamLaunchState === 'partial_failure') { + reconciled.partialLaunchFailure = true; + } else { + delete reconciled.partialLaunchFailure; + } + return reconciled; +} + export function choosePreferredLaunchStateSummary(params: { bootstrapSnapshot?: PersistedTeamLaunchSnapshot | null; launchSnapshot?: PersistedTeamLaunchSnapshot | null; @@ -252,7 +415,9 @@ export function choosePreferredLaunchStateSummary(params: { ? null : (params.launchSnapshot ?? null); if (launchSnapshot) { - return createLaunchStateSummary(launchSnapshot); + return createLaunchStateSummary(launchSnapshot, { + bootstrapSnapshot: params.bootstrapSnapshot ?? null, + }); } const bootstrapSnapshot = params.bootstrapSnapshot ?? null; @@ -271,22 +436,28 @@ export function choosePreferredLaunchStateSummary(params: { return createLaunchStateSummary(bootstrapSnapshot); } + const reconciledProjection = reconcileSummaryProjectionWithBootstrap( + projection, + bootstrapSnapshot + ); const bootstrapMixedAware = hasMixedPersistedLaunchMetadata(bootstrapSnapshot); - const projectionMixedAware = projection.mixedAware === true; + const projectionMixedAware = reconciledProjection.mixedAware === true; if (projectionMixedAware !== bootstrapMixedAware) { - return projectionMixedAware ? projection : createLaunchStateSummary(bootstrapSnapshot); + return projectionMixedAware + ? reconciledProjection + : createLaunchStateSummary(bootstrapSnapshot); } - const projectionUpdatedAtMs = toMillis(projection.updatedAt); + const projectionUpdatedAtMs = toMillis(reconciledProjection.updatedAt); const bootstrapUpdatedAtMs = toMillis(bootstrapSnapshot.updatedAt); if (!Number.isFinite(bootstrapUpdatedAtMs)) { - return projection; + return reconciledProjection; } if (!Number.isFinite(projectionUpdatedAtMs)) { return createLaunchStateSummary(bootstrapSnapshot); } return projectionUpdatedAtMs >= bootstrapUpdatedAtMs - ? projection + ? reconciledProjection : createLaunchStateSummary(bootstrapSnapshot); } diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 1199b685..f20418c9 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -122,6 +122,7 @@ import { isTeamInternalControlMessageText, stripExactInternalControlEchoPrefix, } from '@shared/utils/teamInternalControlMessages'; +import { hasUnsafeProvisionedButNotAliveRuntimeEvidence } from '@shared/utils/teamLaunchFailureReason'; import { parseAllTeammateMessages, type ParsedTeammateContent, @@ -231,6 +232,7 @@ import { isAutoClearableLaunchFailureReason, isCliProvisionedButNotAliveFailureReason, isNeverSpawnedDuringLaunchReason, + isProvisionedButNotAliveFailureReason, } from './provisioning/TeamProvisioningLaunchFailurePolicy'; import { isOpenCodeOverlayMemberRemoved, @@ -959,9 +961,16 @@ function isConfirmedBootstrapStaleRuntimeDiagnostic(reason?: string): boolean { return text === 'persisted runtime pid is not alive'; } +function isBootstrapProofClearableLaunchFailureReason(reason?: string): boolean { + return ( + isAutoClearableLaunchFailureReason(reason) || isProvisionedButNotAliveFailureReason(reason) + ); +} + function shouldClearRuntimeDiagnosticAfterBootstrapConfirmation(reason?: string): boolean { return ( - isAutoClearableLaunchFailureReason(reason) || isConfirmedBootstrapStaleRuntimeDiagnostic(reason) + isBootstrapProofClearableLaunchFailureReason(reason) || + isConfirmedBootstrapStaleRuntimeDiagnostic(reason) ); } @@ -13666,13 +13675,14 @@ export class TeamProvisioningService { status: 'online', updatedAt, agentToolAccepted: true, - runtimeAlive: source === 'runtime-proof' ? true : prev.runtimeAlive === true, + runtimeAlive: source === 'runtime-proof' ? true : prev.runtimeAlive, bootstrapConfirmed: true, hardFailure: false, bootstrapStalled: undefined, error: undefined, hardFailureReason: undefined, - livenessSource: prev.livenessSource ?? 'process', + livenessSource: + source === 'runtime-proof' ? (prev.livenessSource ?? 'process') : prev.livenessSource, firstSpawnAcceptedAt: prev.firstSpawnAcceptedAt ?? observedAt, lastHeartbeatAt: isMemberSpawnHeartbeatTimestampNewer(prev.lastHeartbeatAt, observedAt) ? observedAt @@ -17128,7 +17138,7 @@ export class TeamProvisioningService { const canClearFailedBootstrap = current?.launchState === 'failed_to_start' && current.agentToolAccepted === true && - isAutoClearableLaunchFailureReason(failureReason); + isBootstrapProofClearableLaunchFailureReason(failureReason); if ( !current || (current.launchState === 'failed_to_start' && !canClearFailedBootstrap) || @@ -24243,6 +24253,39 @@ export class TeamProvisioningService { current.bootstrapConfirmed === true || current.launchState === 'confirmed_alive'; const shouldSuppressWeakRuntimeMetadataForConfirmedBootstrap = hasConfirmedBootstrap && !hasStrongEvidence; + const failureReason = current.hardFailureReason ?? current.error ?? current.runtimeDiagnostic; + const bootstrapProofClearableFailure = + isBootstrapProofClearableLaunchFailureReason(failureReason); + const metadataRuntimeDiagnosticForUnsafe = buildRuntimeDiagnosticForSpawn(metadata); + const unsafeRuntimeDiagnosticEvidence = + metadataRuntimeDiagnosticForUnsafe && + current.runtimeDiagnostic && + metadataRuntimeDiagnosticForUnsafe !== current.runtimeDiagnostic + ? `${metadataRuntimeDiagnosticForUnsafe}; ${current.runtimeDiagnostic}` + : (metadataRuntimeDiagnosticForUnsafe ?? current.runtimeDiagnostic); + const hasUnsafeProvisionedButNotAliveFailure = + isProvisionedButNotAliveFailureReason(failureReason) && + hasUnsafeProvisionedButNotAliveRuntimeEvidence({ + ...current, + runtimeDiagnostic: unsafeRuntimeDiagnosticEvidence, + runtimeDiagnosticSeverity: + metadata.runtimeDiagnosticSeverity ?? current.runtimeDiagnosticSeverity, + livenessKind: metadata.livenessKind ?? current.livenessKind, + }); + const shouldPreserveConfirmedBootstrapRuntimeError = + hasConfirmedBootstrap && + metadata.alive === false && + metadata.runtimeDiagnosticSeverity === 'error'; + const shouldPreserveUnsafeMetadataLivenessKind = + hasUnsafeProvisionedButNotAliveFailure && + (metadata.livenessKind === 'not_found' || + metadata.livenessKind === 'shell_only' || + metadata.livenessKind === 'runtime_process_candidate' || + ((metadata.livenessKind === 'registered_only' || + metadata.livenessKind === 'stale_metadata') && + (metadata.runtimeDiagnosticSeverity ?? current.runtimeDiagnosticSeverity) !== 'error' && + !mentionsProcessTableUnavailable(unsafeRuntimeDiagnosticEvidence) && + !mentionsProcessTableUnavailable(failureReason))); let runtimeDiagnostic: string | undefined; let runtimeDiagnosticSeverity: TeamAgentRuntimeDiagnosticSeverity | undefined; if (shouldPreserveProcessBootstrapTransportDiagnostic) { @@ -24256,7 +24299,7 @@ export class TeamProvisioningService { runtimeDiagnostic = current.runtimeDiagnostic; runtimeDiagnosticSeverity = current.runtimeDiagnosticSeverity; } else { - const metadataRuntimeDiagnostic = buildRuntimeDiagnosticForSpawn(metadata); + const metadataRuntimeDiagnostic = metadataRuntimeDiagnosticForUnsafe; if ( metadataRuntimeDiagnostic && !shouldClearRuntimeDiagnosticAfterBootstrapConfirmation(metadataRuntimeDiagnostic) @@ -24271,7 +24314,9 @@ export class TeamProvisioningService { } const metadataLivenessKind = hasConfirmedBootstrap ? metadata.livenessKind === 'runtime_process' || - metadata.livenessKind === 'confirmed_bootstrap' + metadata.livenessKind === 'confirmed_bootstrap' || + shouldPreserveConfirmedBootstrapRuntimeError || + shouldPreserveUnsafeMetadataLivenessKind ? metadata.livenessKind : current.livenessKind === 'stale_metadata' || current.livenessKind === 'registered_only' ? 'confirmed_bootstrap' @@ -24291,7 +24336,6 @@ export class TeamProvisioningService { : {}), livenessLastCheckedAt: nowIso(), }; - const failureReason = current.hardFailureReason ?? current.error; const hasWeakEvidence = metadata.livenessKind != null && !hasStrongEvidence && current.bootstrapConfirmed !== true; if ( @@ -24335,7 +24379,8 @@ export class TeamProvisioningService { if ( hasStrongEvidence && current.launchState === 'failed_to_start' && - isAutoClearableLaunchFailureReason(failureReason) + bootstrapProofClearableFailure && + !hasUnsafeProvisionedButNotAliveFailure ) { nextEntry.status = 'online'; nextEntry.agentToolAccepted = true; @@ -24346,7 +24391,34 @@ export class TeamProvisioningService { nextEntry.livenessSource = current.bootstrapConfirmed ? current.livenessSource : 'process'; nextEntry.launchState = deriveMemberLaunchState(nextEntry); } - if (hasWeakEvidence) { + if ( + hasConfirmedBootstrap && + current.hardFailure === true && + bootstrapProofClearableFailure && + !hasUnsafeProvisionedButNotAliveFailure + ) { + nextEntry.status = 'online'; + nextEntry.agentToolAccepted = true; + nextEntry.runtimeAlive = true; + nextEntry.bootstrapConfirmed = true; + nextEntry.hardFailure = false; + nextEntry.hardFailureReason = undefined; + nextEntry.error = undefined; + nextEntry.bootstrapStalled = undefined; + nextEntry.launchState = deriveMemberLaunchState(nextEntry); + } + const healedConfirmedBootstrapFailure = + hasConfirmedBootstrap && + current.hardFailure === true && + bootstrapProofClearableFailure && + !hasUnsafeProvisionedButNotAliveFailure; + if (shouldPreserveConfirmedBootstrapRuntimeError) { + nextEntry.runtimeAlive = false; + if (nextEntry.livenessSource === 'process') { + nextEntry.livenessSource = undefined; + } + } + if (hasWeakEvidence && !healedConfirmedBootstrapFailure) { nextEntry.runtimeAlive = false; if (nextEntry.livenessSource === 'process') { nextEntry.livenessSource = undefined; @@ -26714,10 +26786,17 @@ export class TeamProvisioningService { if (current.launchState === 'skipped_for_launch' || current.skippedForLaunch === true) { continue; } - const failureReason = current.hardFailureReason ?? current.error; + const failureReason = current.hardFailureReason ?? current.error ?? current.runtimeDiagnostic; + const provisionedButNotAliveFailure = isProvisionedButNotAliveFailureReason(failureReason); + if ( + provisionedButNotAliveFailure && + hasUnsafeProvisionedButNotAliveRuntimeEvidence(current) + ) { + continue; + } if ( current.launchState === 'failed_to_start' && - !isAutoClearableLaunchFailureReason(failureReason) + !isBootstrapProofClearableLaunchFailureReason(failureReason) ) { continue; } @@ -26827,12 +26906,19 @@ export class TeamProvisioningService { : undefined; const failureReason = current.hardFailureReason ?? persistedError ?? current.runtimeDiagnostic; + const provisionedButNotAliveFailure = isProvisionedButNotAliveFailureReason(failureReason); + if ( + provisionedButNotAliveFailure && + hasUnsafeProvisionedButNotAliveRuntimeEvidence(current) + ) { + continue; + } const hasFailure = current.launchState === 'failed_to_start' || current.hardFailure === true || typeof current.hardFailureReason === 'string' || typeof persistedError === 'string'; - if (hasFailure && !isAutoClearableLaunchFailureReason(failureReason)) { + if (hasFailure && !isBootstrapProofClearableLaunchFailureReason(failureReason)) { continue; } @@ -26845,14 +26931,21 @@ export class TeamProvisioningService { ...current, launchState: 'confirmed_alive', agentToolAccepted: true, - runtimeAlive: current.runtimeAlive === true || bootstrapMember.runtimeAlive === true, + runtimeAlive: + current.runtimeAlive === true || + bootstrapMember.runtimeAlive === true || + provisionedButNotAliveFailure, bootstrapConfirmed: true, hardFailure: false, hardFailureReason: undefined, - runtimeDiagnostic: isAutoClearableLaunchFailureReason(current.runtimeDiagnostic) + runtimeDiagnostic: shouldClearRuntimeDiagnosticAfterBootstrapConfirmation( + current.runtimeDiagnostic + ) ? undefined : current.runtimeDiagnostic, - runtimeDiagnosticSeverity: isAutoClearableLaunchFailureReason(current.runtimeDiagnostic) + runtimeDiagnosticSeverity: shouldClearRuntimeDiagnosticAfterBootstrapConfirmation( + current.runtimeDiagnostic + ) ? undefined : current.runtimeDiagnosticSeverity, bootstrapStalled: undefined, @@ -28638,7 +28731,9 @@ export class TeamProvisioningService { current.firstSpawnAcceptedAt != null ? Date.parse(current.firstSpawnAcceptedAt) : NaN; if ( current.launchState !== 'failed_to_start' || - isAutoClearableLaunchFailureReason(current.hardFailureReason ?? current.runtimeDiagnostic) + isBootstrapProofClearableLaunchFailureReason( + current.hardFailureReason ?? current.runtimeDiagnostic + ) ) { const runtimeProofObservedAt = await this.findBootstrapRuntimeProofObservedAt( snapshot.teamName, @@ -29053,9 +29148,16 @@ export class TeamProvisioningService { continue; } const failureReason = current.hardFailureReason ?? current.runtimeDiagnostic; + const provisionedButNotAliveFailure = isProvisionedButNotAliveFailureReason(failureReason); + if ( + provisionedButNotAliveFailure && + hasUnsafeProvisionedButNotAliveRuntimeEvidence(current) + ) { + continue; + } const canClearFailedBootstrap = current.launchState !== 'failed_to_start' || - isAutoClearableLaunchFailureReason(failureReason); + isBootstrapProofClearableLaunchFailureReason(failureReason); if (!canClearFailedBootstrap) { continue; } @@ -29083,7 +29185,9 @@ export class TeamProvisioningService { ...current, agentToolAccepted: true, bootstrapConfirmed: true, - runtimeAlive: runtimeProofObservedAt ? true : current.runtimeAlive === true, + runtimeAlive: runtimeProofObservedAt + ? true + : current.runtimeAlive === true || provisionedButNotAliveFailure, hardFailure: false, hardFailureReason: undefined, lastHeartbeatAt: current.lastHeartbeatAt ?? observedAt, @@ -29140,7 +29244,7 @@ export class TeamProvisioningService { const failureReason = current.hardFailureReason ?? current.runtimeDiagnostic; const hasAutoClearableFailure = (current.launchState === 'failed_to_start' || current.hardFailure === true) && - isAutoClearableLaunchFailureReason(failureReason); + isBootstrapProofClearableLaunchFailureReason(failureReason); if (!currentConfirmed || hasAutoClearableFailure) { return true; } @@ -29537,12 +29641,58 @@ export class TeamProvisioningService { const hadAutoClearableFailure = isAutoClearableLaunchFailureReason(initialFailureReason); const requiresConfirmedBootstrapToClearFailure = isCliProvisionedButNotAliveFailureReason(initialFailureReason); + const metadataRuntimeDiagnostic = runtimeMetadata?.[1].runtimeDiagnostic; + const metadataRuntimeDiagnosticSeverity = runtimeMetadata?.[1].runtimeDiagnosticSeverity; + const metadataLivenessKind = runtimeMetadata?.[1].livenessKind; + const refreshedRuntimeDiagnosticEvidence = + metadataRuntimeDiagnostic && + current.runtimeDiagnostic && + metadataRuntimeDiagnostic !== current.runtimeDiagnostic + ? `${metadataRuntimeDiagnostic}; ${current.runtimeDiagnostic}` + : (metadataRuntimeDiagnostic ?? current.runtimeDiagnostic); + const hasUnsafeProvisionedButNotAliveFailure = + requiresConfirmedBootstrapToClearFailure && + hasUnsafeProvisionedButNotAliveRuntimeEvidence({ + ...current, + runtimeDiagnostic: refreshedRuntimeDiagnosticEvidence, + runtimeDiagnosticSeverity: + metadataRuntimeDiagnosticSeverity ?? current.runtimeDiagnosticSeverity, + livenessKind: metadataLivenessKind ?? current.livenessKind, + }); + const shouldPreserveUnsafeMetadataLivenessKind = + hasUnsafeProvisionedButNotAliveFailure && + (metadataLivenessKind === 'not_found' || + metadataLivenessKind === 'shell_only' || + metadataLivenessKind === 'runtime_process_candidate' || + ((metadataLivenessKind === 'registered_only' || + metadataLivenessKind === 'stale_metadata') && + (metadataRuntimeDiagnosticSeverity ?? current.runtimeDiagnosticSeverity) !== 'error' && + !mentionsProcessTableUnavailable(refreshedRuntimeDiagnosticEvidence) && + !mentionsProcessTableUnavailable(initialFailureReason))); + const nextLivenessKind = current.bootstrapConfirmed + ? metadataLivenessKind === 'runtime_process' || + metadataLivenessKind === 'confirmed_bootstrap' || + shouldPreserveUnsafeMetadataLivenessKind + ? metadataLivenessKind + : current.livenessKind === 'stale_metadata' || current.livenessKind === 'registered_only' + ? 'confirmed_bootstrap' + : (current.livenessKind ?? 'confirmed_bootstrap') + : (metadataLivenessKind ?? current.livenessKind); current.runtimeAlive = observedRuntimeAlive; current.lastRuntimeAliveAt = observedRuntimeAlive ? now : current.lastRuntimeAliveAt; - current.livenessKind = runtimeMetadata?.[1].livenessKind; + current.livenessKind = nextLivenessKind; current.pidSource = runtimeMetadata?.[1].pidSource; - current.runtimeDiagnostic = runtimeMetadata?.[1].runtimeDiagnostic; - current.runtimeDiagnosticSeverity = runtimeMetadata?.[1].runtimeDiagnosticSeverity; + const shouldKeepUnsafeRuntimeDiagnostic = + hasUnsafeProvisionedButNotAliveFailure && + (metadataRuntimeDiagnostic == null || + (current.runtimeDiagnosticSeverity === 'error' && + metadataRuntimeDiagnosticSeverity !== 'error')); + current.runtimeDiagnostic = shouldKeepUnsafeRuntimeDiagnostic + ? current.runtimeDiagnostic + : metadataRuntimeDiagnostic; + current.runtimeDiagnosticSeverity = shouldKeepUnsafeRuntimeDiagnostic + ? current.runtimeDiagnosticSeverity + : metadataRuntimeDiagnosticSeverity; current.sources = { ...(current.sources ?? {}), processAlive: observedRuntimeAlive || undefined, @@ -29572,8 +29722,12 @@ export class TeamProvisioningService { if ( current.bootstrapConfirmed && !isOpenCodeSecondaryLaneMember && - isAutoClearableLaunchFailureReason(current.hardFailureReason) + !hasUnsafeProvisionedButNotAliveFailure && + isBootstrapProofClearableLaunchFailureReason(current.hardFailureReason) ) { + if (isProvisionedButNotAliveFailureReason(current.hardFailureReason)) { + current.runtimeAlive = true; + } current.hardFailure = false; current.hardFailureReason = undefined; if (current.sources) { @@ -29592,9 +29746,10 @@ export class TeamProvisioningService { } const canApplyBootstrapSuccess = !heartbeatReason && + !hasUnsafeProvisionedButNotAliveFailure && (current.launchState !== 'failed_to_start' || hadAutoClearableFailure || - isAutoClearableLaunchFailureReason( + isBootstrapProofClearableLaunchFailureReason( current.hardFailureReason ?? current.runtimeDiagnostic )); if (!current.bootstrapConfirmed && canApplyBootstrapSuccess) { @@ -29614,7 +29769,9 @@ export class TeamProvisioningService { if (bootstrapObservedAt && !isOpenCodeSecondaryLaneMember) { current.bootstrapConfirmed = true; current.lastHeartbeatAt = current.lastHeartbeatAt ?? bootstrapObservedAt; - current.runtimeAlive = runtimeProofObservedAt ? true : current.runtimeAlive === true; + current.runtimeAlive = runtimeProofObservedAt + ? true + : current.runtimeAlive === true || requiresConfirmedBootstrapToClearFailure; current.lastRuntimeAliveAt = runtimeProofObservedAt ? (current.lastRuntimeAliveAt ?? bootstrapObservedAt) : current.lastRuntimeAliveAt; diff --git a/src/main/services/team/provisioning/TeamProvisioningLaunchDiagnostics.ts b/src/main/services/team/provisioning/TeamProvisioningLaunchDiagnostics.ts index 4eb1290f..ccac46c0 100644 --- a/src/main/services/team/provisioning/TeamProvisioningLaunchDiagnostics.ts +++ b/src/main/services/team/provisioning/TeamProvisioningLaunchDiagnostics.ts @@ -1,6 +1,14 @@ +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidence, + isBootstrapConfirmedProvisionedButNotAliveFailure, + mentionsProcessTableUnavailable, +} from '@shared/utils/teamLaunchFailureReason'; + import type { WorkspaceTrustExecutionResult } from '@features/workspace-trust/main'; import type { MemberSpawnStatusEntry, TeamLaunchDiagnosticItem } from '@shared/types'; +export { mentionsProcessTableUnavailable }; + export interface TeamProvisioningLaunchDiagnosticsRun { isLaunch: boolean; memberSpawnStatuses?: ReadonlyMap | null; @@ -12,10 +20,6 @@ interface LaunchDiagnosticsClockOptions { const defaultNowIso = (): string => new Date().toISOString(); -export function mentionsProcessTableUnavailable(value: string | undefined): boolean { - return /\bprocess table\b.*\bunavailable\b/i.test(value ?? ''); -} - export function buildLaunchDiagnosticsFromRun( run: TeamProvisioningLaunchDiagnosticsRun, options: LaunchDiagnosticsClockOptions = {} @@ -28,7 +32,24 @@ export function buildLaunchDiagnosticsFromRun( const observedAt = (options.nowIso ?? defaultNowIso)(); const items: TeamLaunchDiagnosticItem[] = []; for (const [memberName, entry] of memberSpawnStatuses.entries()) { - if (entry.launchState === 'confirmed_alive') { + const bootstrapConfirmedProvisionedButNotAlive = + isBootstrapConfirmedProvisionedButNotAliveFailure(entry); + if ( + bootstrapConfirmedProvisionedButNotAlive && + hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry) + ) { + items.push({ + id: `${memberName}:bootstrap_stalled`, + memberName, + severity: 'error', + code: 'bootstrap_stalled', + label: `${memberName} - launch diagnostic error`, + detail: entry.runtimeDiagnostic ?? entry.hardFailureReason ?? entry.error, + observedAt, + }); + continue; + } + if (entry.launchState === 'confirmed_alive' || bootstrapConfirmedProvisionedButNotAlive) { items.push({ id: `${memberName}:bootstrap_confirmed`, memberName, diff --git a/src/main/services/team/provisioning/TeamProvisioningLaunchFailurePolicy.ts b/src/main/services/team/provisioning/TeamProvisioningLaunchFailurePolicy.ts index 2f6366c0..be073191 100644 --- a/src/main/services/team/provisioning/TeamProvisioningLaunchFailurePolicy.ts +++ b/src/main/services/team/provisioning/TeamProvisioningLaunchFailurePolicy.ts @@ -1,6 +1,17 @@ +import { + isProvisionedButNotAliveFailureReason, + stripProcessTableUnavailableDiagnosticSuffix, +} from '@shared/utils/teamLaunchFailureReason'; + import { mentionsProcessTableUnavailable } from './TeamProvisioningLaunchDiagnostics'; import { isBootstrapInstructionPrompt } from './TeamProvisioningPromptBuilders'; +export { + isCliProvisionedButNotAliveFailureReason, + isProvisionedButNotAliveFailureReason, + stripProcessTableUnavailableDiagnosticSuffix, +} from '@shared/utils/teamLaunchFailureReason'; + import type { MemberLaunchState } from '@shared/types'; export function isNeverSpawnedDuringLaunchReason(reason?: string): boolean { @@ -37,22 +48,6 @@ export function isProcessTableUnavailableFailureReason(reason?: string): boolean ); } -export function isCliProvisionedButNotAliveFailureReason(reason?: string): boolean { - const text = reason?.trim(); - if (!text) { - return false; - } - return /^CLI process exited \(code (?:unknown|\d+|\?)\) [\u2014-] team provisioned but not alive$/i.test( - text - ); -} - -export function stripProcessTableUnavailableDiagnosticSuffix(reason: string): string | null { - const match = /^(.*?);\s*process table (?:is )?unavailable$/i.exec(reason.trim()); - const baseReason = match?.[1]?.trim(); - return baseReason && baseReason.length > 0 ? baseReason : null; -} - function isBaseAutoClearableLaunchFailureReason(reason?: string): boolean { return ( isNeverSpawnedDuringLaunchReason(reason) || @@ -63,8 +58,7 @@ function isBaseAutoClearableLaunchFailureReason(reason?: string): boolean { isBootstrapMcpResourceReadFailureReason(reason) || isBootstrapCheckInTimeoutFailureReason(reason) || isBootstrapInstructionPromptFailureReason(reason) || - isLaunchCleanupBootstrapIncompleteFailureReason(reason) || - isCliProvisionedButNotAliveFailureReason(reason) + isLaunchCleanupBootstrapIncompleteFailureReason(reason) ); } diff --git a/src/main/services/team/provisioning/TeamProvisioningPromptBuilders.ts b/src/main/services/team/provisioning/TeamProvisioningPromptBuilders.ts index a4ce7a4c..4bc8c574 100644 --- a/src/main/services/team/provisioning/TeamProvisioningPromptBuilders.ts +++ b/src/main/services/team/provisioning/TeamProvisioningPromptBuilders.ts @@ -2,6 +2,10 @@ import { resolveTeamProviderId } from '@main/services/runtime/providerRuntimeEnv import { AGENT_BLOCK_CLOSE, AGENT_BLOCK_OPEN, wrapAgentBlock } from '@shared/constants/agentBlocks'; import { CROSS_TEAM_PREFIX_TAG } from '@shared/constants/crossTeam'; import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidence, + isBootstrapConfirmedProvisionedButNotAliveFailure, +} from '@shared/utils/teamLaunchFailureReason'; import { getTeamTaskWorkflowColumn, isTeamTaskActivelyWorked, @@ -44,6 +48,57 @@ interface CanonicalSendMessageExample { const SEND_MESSAGE_CANONICAL_FIELDS = ['to', 'summary', 'message'] as const; const SEND_MESSAGE_FORBIDDEN_ALIAS_FIELDS = ['recipient', 'content'] as const; +function isUnsafeProvisionedButNotAliveStatus(status: MemberSpawnStatusEntry | undefined) { + return ( + isBootstrapConfirmedProvisionedButNotAliveFailure(status) && + hasUnsafeProvisionedButNotAliveRuntimeEvidence(status) + ); +} + +function isSafelyHealedProvisionedButNotAliveStatus(status: MemberSpawnStatusEntry | undefined) { + return ( + isBootstrapConfirmedProvisionedButNotAliveFailure(status) && + !isUnsafeProvisionedButNotAliveStatus(status) + ); +} + +function formatFailedLaunchStatus(status: MemberSpawnStatusEntry): string { + return `failed to start${status.hardFailureReason ? ` - ${status.hardFailureReason}` : status.error ? ` - ${status.error}` : ''}`; +} + +function buildTeammateLaunchStatusLabel(status: MemberSpawnStatusEntry | undefined): string { + if (!status) { + return 'runtime state unclear'; + } + if ( + status.launchState === 'failed_to_start' && + !isSafelyHealedProvisionedButNotAliveStatus(status) + ) { + return formatFailedLaunchStatus(status); + } + if ( + status.launchState === 'confirmed_alive' || + isSafelyHealedProvisionedButNotAliveStatus(status) + ) { + return 'bootstrap confirmed'; + } + if (status.launchState === 'runtime_pending_permission') { + return status.runtimeAlive + ? 'runtime online and waiting for permission approval' + : 'waiting for permission approval'; + } + if (status.runtimeAlive) { + return 'runtime online and ready for instructions'; + } + if (status.launchState === 'runtime_pending_bootstrap') { + return 'spawn accepted, runtime not confirmed yet'; + } + if (status.status === 'spawning') { + return 'spawn in progress'; + } + return 'runtime state unclear'; +} + export function buildCanonicalSendMessageExample(example: CanonicalSendMessageExample): string { return `{ ${SEND_MESSAGE_CANONICAL_FIELDS.map((field) => `${field}: "${example[field]}"`).join(', ')} }`; } @@ -1037,22 +1092,7 @@ export function buildGeminiPostLaunchHydrationPrompt( ? `Current teammate launch status:\n${members .map((member) => { const status = run.memberSpawnStatuses.get(member.name); - const label = - status?.launchState === 'failed_to_start' - ? `failed to start${status.hardFailureReason ? ` - ${status.hardFailureReason}` : status.error ? ` - ${status.error}` : ''}` - : status?.launchState === 'confirmed_alive' - ? 'bootstrap confirmed' - : status?.launchState === 'runtime_pending_permission' - ? status?.runtimeAlive - ? 'runtime online and waiting for permission approval' - : 'waiting for permission approval' - : status?.runtimeAlive - ? 'runtime online and ready for instructions' - : status?.launchState === 'runtime_pending_bootstrap' - ? 'spawn accepted, runtime not confirmed yet' - : status?.status === 'spawning' - ? 'spawn in progress' - : 'runtime state unclear'; + const label = buildTeammateLaunchStatusLabel(status); return `- @${member.name}: ${label}`; }) .join('\n')}\n` diff --git a/src/renderer/components/team/members/MemberCard.tsx b/src/renderer/components/team/members/MemberCard.tsx index 9cfd5332..034b0373 100644 --- a/src/renderer/components/team/members/MemberCard.tsx +++ b/src/renderer/components/team/members/MemberCard.tsx @@ -28,6 +28,10 @@ import { import { getRuntimeMemorySourceLabel } from '@renderer/utils/memberRuntimeSummary'; import { isLeadMember } from '@shared/utils/leadDetection'; import { deriveTaskDisplayId } from '@shared/utils/taskIdentity'; +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext, + isBootstrapConfirmedProvisionedButNotAliveFailure, +} from '@shared/utils/teamLaunchFailureReason'; import { Activity, AlertTriangle, @@ -661,12 +665,20 @@ export const MemberCard = memo(function MemberCard({ selectedTeamName ? selectResolvedMembersForTeamName(s, selectedTeamName) : [] ); const avatarMap = useMemo(() => buildMemberAvatarMap(teamMembers), [teamMembers]); + const bootstrapConfirmedProvisionedButNotAlive = + isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry); + const hasUnsafeBootstrapConfirmedProvisionedButNotAlive = + bootstrapConfirmedProvisionedButNotAlive && + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawnEntry, runtimeEntry); + const effectiveSpawnStatus = spawnStatus; + const effectiveSpawnLaunchState = spawnLaunchState; const showTaskActivity = shouldDisplayMemberCurrentTask({ member, isTeamAlive, - spawnStatus, - spawnLaunchState, + spawnStatus: effectiveSpawnStatus, + spawnLaunchState: effectiveSpawnLaunchState, spawnRuntimeAlive, + spawnEntry, runtimeEntry, }); const visibleCurrentTask = showTaskActivity ? currentTask : null; @@ -680,15 +692,19 @@ export const MemberCard = memo(function MemberCard({ : member; const launchPresentation = buildMemberLaunchPresentation({ member: presentationMember, - spawnStatus, - spawnLaunchState, + spawnStatus: effectiveSpawnStatus, + spawnLaunchState: effectiveSpawnLaunchState, spawnLivenessSource, spawnRuntimeAlive, spawnBootstrapConfirmed: spawnEntry?.bootstrapConfirmed, spawnBootstrapStalled: spawnEntry?.bootstrapStalled, spawnAgentToolAccepted: spawnEntry?.agentToolAccepted, spawnHardFailure: spawnEntry?.hardFailure, + spawnHardFailureReason: spawnEntry?.hardFailureReason, + spawnError: spawnEntry?.error, + spawnRuntimeDiagnostic: spawnEntry?.runtimeDiagnostic, spawnLivenessKind: spawnEntry?.livenessKind, + spawnRuntimeDiagnosticSeverity: spawnEntry?.runtimeDiagnosticSeverity, spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt, spawnUpdatedAt: spawnEntry?.updatedAt, runtimeEntry, @@ -844,7 +860,7 @@ export const MemberCard = memo(function MemberCard({ const showStartingSkeleton = !isRemoved && presenceLabel === 'starting' && - spawnLaunchState !== 'failed_to_start' && + effectiveSpawnLaunchState !== 'failed_to_start' && !activityTask && !runtimeSummary; const usesLaunchSkeletonSurface = spawnCardClass.includes('member-waiting-shimmer'); @@ -869,8 +885,8 @@ export const MemberCard = memo(function MemberCard({ runId: runtimeRunId, memberName: member.name, member, - spawnStatus, - launchState: spawnLaunchState, + spawnStatus: effectiveSpawnStatus, + launchState: effectiveSpawnLaunchState, livenessSource: spawnLivenessSource, spawnEntry, runtimeEntry, @@ -886,9 +902,9 @@ export const MemberCard = memo(function MemberCard({ runtimeRunId, selectedTeamName, spawnEntry, - spawnLaunchState, + effectiveSpawnLaunchState, spawnLivenessSource, - spawnStatus, + effectiveSpawnStatus, ] ); const showCopyDiagnostics = @@ -900,7 +916,10 @@ export const MemberCard = memo(function MemberCard({ Boolean(runtimeAdvisoryLabel) && runtimeAdvisoryTone === 'error' && hasMemberLaunchDiagnosticsDetails(launchDiagnosticsPayload); - const isFailedLaunch = spawnStatus === 'error' || spawnLaunchState === 'failed_to_start'; + const isFailedLaunch = + (!bootstrapConfirmedProvisionedButNotAlive || + hasUnsafeBootstrapConfirmedProvisionedButNotAlive) && + (spawnStatus === 'error' || spawnLaunchState === 'failed_to_start'); const isSkippedLaunch = spawnStatus === 'skipped' || spawnLaunchState === 'skipped_for_launch' || diff --git a/src/renderer/components/team/members/MemberDetailDialog.tsx b/src/renderer/components/team/members/MemberDetailDialog.tsx index 0a1ee0ec..fa80c2b8 100644 --- a/src/renderer/components/team/members/MemberDetailDialog.tsx +++ b/src/renderer/components/team/members/MemberDetailDialog.tsx @@ -24,6 +24,10 @@ import { } from '@renderer/utils/memberRuntimeSummary'; import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState'; import { isLeadMember } from '@shared/utils/leadDetection'; +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext, + isBootstrapConfirmedProvisionedButNotAliveFailure, +} from '@shared/utils/teamLaunchFailureReason'; import { isTeamTaskFinishedForDependency } from '@shared/utils/teamTaskState'; import { BarChart3, @@ -83,7 +87,14 @@ function isOpenCodeNoRuntimeEvidenceFailure( spawnEntry: MemberSpawnStatusEntry | undefined, runtimeEntry: TeamAgentRuntimeEntry | undefined ): boolean { - const failed = spawnEntry?.launchState === 'failed_to_start' || spawnEntry?.status === 'error'; + const bootstrapConfirmedProvisionedButNotAlive = + isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry); + const unsafeProvisionedButNotAlive = + bootstrapConfirmedProvisionedButNotAlive && + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawnEntry, runtimeEntry); + const failed = + (!bootstrapConfirmedProvisionedButNotAlive || unsafeProvisionedButNotAlive) && + (spawnEntry?.launchState === 'failed_to_start' || spawnEntry?.status === 'error'); return member.providerId === 'opencode' && failed && !hasOpenCodeRuntimeEvidence(runtimeEntry); } @@ -180,6 +191,7 @@ export const MemberDetailDialog = ({ spawnStatus: spawnEntry?.status, spawnLaunchState: spawnEntry?.launchState, spawnRuntimeAlive: spawnEntry?.runtimeAlive, + spawnEntry, runtimeEntry, }); const displayableCurrentTask = @@ -303,7 +315,11 @@ export const MemberDetailDialog = ({ spawnBootstrapStalled={spawnEntry?.bootstrapStalled} spawnAgentToolAccepted={spawnEntry?.agentToolAccepted} spawnHardFailure={spawnEntry?.hardFailure} + spawnHardFailureReason={spawnEntry?.hardFailureReason} + spawnError={spawnEntry?.error} + spawnRuntimeDiagnostic={spawnEntry?.runtimeDiagnostic} spawnLivenessKind={spawnEntry?.livenessKind} + spawnRuntimeDiagnosticSeverity={spawnEntry?.runtimeDiagnosticSeverity} spawnFirstSpawnAcceptedAt={spawnEntry?.firstSpawnAcceptedAt} spawnUpdatedAt={spawnEntry?.updatedAt} runtimeEntry={runtimeEntry} diff --git a/src/renderer/components/team/members/MemberDetailHeader.tsx b/src/renderer/components/team/members/MemberDetailHeader.tsx index b1c9acb7..e61d3a17 100644 --- a/src/renderer/components/team/members/MemberDetailHeader.tsx +++ b/src/renderer/components/team/members/MemberDetailHeader.tsx @@ -25,6 +25,7 @@ import type { MemberSpawnLivenessSource, MemberSpawnStatus, ResolvedTeamMember, + TeamAgentRuntimeDiagnosticSeverity, TeamAgentRuntimeEntry, } from '@shared/types'; @@ -43,7 +44,11 @@ interface MemberDetailHeaderProps { spawnBootstrapStalled?: boolean; spawnAgentToolAccepted?: boolean; spawnHardFailure?: boolean; + spawnHardFailureReason?: string; + spawnError?: string; + spawnRuntimeDiagnostic?: string; spawnLivenessKind?: TeamAgentRuntimeEntry['livenessKind']; + spawnRuntimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity; spawnFirstSpawnAcceptedAt?: string; spawnUpdatedAt?: string; isLaunchSettling?: boolean; @@ -66,7 +71,11 @@ export const MemberDetailHeader = ({ spawnBootstrapStalled, spawnAgentToolAccepted, spawnHardFailure, + spawnHardFailureReason, + spawnError, + spawnRuntimeDiagnostic, spawnLivenessKind, + spawnRuntimeDiagnosticSeverity, spawnFirstSpawnAcceptedAt, spawnUpdatedAt, isLaunchSettling, @@ -99,7 +108,11 @@ export const MemberDetailHeader = ({ spawnBootstrapStalled, spawnAgentToolAccepted, spawnHardFailure, + spawnHardFailureReason, + spawnError, + spawnRuntimeDiagnostic, spawnLivenessKind, + spawnRuntimeDiagnosticSeverity, spawnFirstSpawnAcceptedAt, spawnUpdatedAt, runtimeEntry, diff --git a/src/renderer/components/team/members/MemberHoverCard.tsx b/src/renderer/components/team/members/MemberHoverCard.tsx index 0bc94f54..8957e3d3 100644 --- a/src/renderer/components/team/members/MemberHoverCard.tsx +++ b/src/renderer/components/team/members/MemberHoverCard.tsx @@ -147,6 +147,7 @@ export const MemberHoverCard = memo(function MemberHoverCard({ spawnStatus: spawnEntry?.status, spawnLaunchState: spawnEntry?.launchState, spawnRuntimeAlive: spawnEntry?.runtimeAlive, + spawnEntry, runtimeEntry, }) ? currentTaskCandidate @@ -168,7 +169,11 @@ export const MemberHoverCard = memo(function MemberHoverCard({ spawnBootstrapStalled: spawnEntry?.bootstrapStalled, spawnAgentToolAccepted: spawnEntry?.agentToolAccepted, spawnHardFailure: spawnEntry?.hardFailure, + spawnHardFailureReason: spawnEntry?.hardFailureReason, + spawnError: spawnEntry?.error, + spawnRuntimeDiagnostic: spawnEntry?.runtimeDiagnostic, spawnLivenessKind: spawnEntry?.livenessKind, + spawnRuntimeDiagnosticSeverity: spawnEntry?.runtimeDiagnosticSeverity, spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt, spawnUpdatedAt: spawnEntry?.updatedAt, runtimeEntry, @@ -226,6 +231,7 @@ export const MemberHoverCard = memo(function MemberHoverCard({ spawnStatus: spawnEntry?.status, spawnLaunchState: spawnEntry?.launchState, spawnRuntimeAlive: spawnEntry?.runtimeAlive, + spawnEntry, runtimeEntry, }) ? reviewTaskCandidate diff --git a/src/renderer/components/team/members/MemberList.tsx b/src/renderer/components/team/members/MemberList.tsx index 5e3732a9..5f6ea327 100644 --- a/src/renderer/components/team/members/MemberList.tsx +++ b/src/renderer/components/team/members/MemberList.tsx @@ -11,6 +11,10 @@ import { buildMemberColorMap, shouldDisplayMemberCurrentTask } from '@renderer/u import { resolveMemberRuntimeSummary } from '@renderer/utils/memberRuntimeSummary'; import { isDisplayableCurrentTask } from '@renderer/utils/teamTaskDisplayState'; import { isLeadMember } from '@shared/utils/leadDetection'; +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext, + isBootstrapConfirmedProvisionedButNotAliveFailure, +} from '@shared/utils/teamLaunchFailureReason'; import { getTeamTaskWorkflowColumn } from '@shared/utils/teamTaskState'; import { MemberCard, type RuntimeTelemetryScale } from './MemberCard'; @@ -785,6 +789,7 @@ export const MemberList = memo(function MemberList({ spawnStatus: spawnEntry?.status, spawnLaunchState: spawnEntry?.launchState, spawnRuntimeAlive: spawnEntry?.runtimeAlive, + spawnEntry, runtimeEntry, }); }, @@ -837,6 +842,7 @@ export const MemberList = memo(function MemberList({ spawnStatus: spawnEntry?.status, spawnLaunchState: spawnEntry?.launchState, spawnRuntimeAlive: spawnEntry?.runtimeAlive, + spawnEntry, runtimeEntry, }); syncMemberActivityTimer({ @@ -924,6 +930,32 @@ export const MemberList = memo(function MemberList({ {activeMembers.map((member) => { const spawnEntry = memberSpawnStatuses?.get(member.name); const runtimeEntry = memberRuntimeEntries?.get(member.name); + const bootstrapConfirmedProvisionedButNotAlive = + isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry); + const hasUnsafeProvisionedButNotAliveEvidence = + bootstrapConfirmedProvisionedButNotAlive && + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext( + spawnEntry, + runtimeEntry + ); + const canPromoteBootstrapConfirmedVisualState = + bootstrapConfirmedProvisionedButNotAlive && + spawnEntry?.runtimeDiagnosticSeverity !== 'error' && + runtimeEntry?.runtimeDiagnosticSeverity !== 'error' && + !hasUnsafeProvisionedButNotAliveEvidence; + const effectiveSpawnStatus = canPromoteBootstrapConfirmedVisualState + ? 'online' + : spawnEntry?.status; + const effectiveSpawnLaunchState = canPromoteBootstrapConfirmedVisualState + ? 'confirmed_alive' + : spawnEntry?.launchState; + const useBootstrapConfirmedRuntimeAlive = + canPromoteBootstrapConfirmedVisualState && + runtimeEntry?.runtimeDiagnosticSeverity !== 'error' && + spawnEntry?.runtimeDiagnosticSeverity !== 'error'; + const effectiveSpawnRuntimeAlive = useBootstrapConfirmedRuntimeAlive + ? true + : spawnEntry?.runtimeAlive; const currentTaskCandidate = member.currentTaskId && taskMap ? (taskMap.get(member.currentTaskId) ?? null) : null; const currentTask = @@ -931,9 +963,10 @@ export const MemberList = memo(function MemberList({ shouldDisplayMemberCurrentTask({ member, isTeamAlive, - spawnStatus: spawnEntry?.status, - spawnLaunchState: spawnEntry?.launchState, - spawnRuntimeAlive: spawnEntry?.runtimeAlive, + spawnStatus: effectiveSpawnStatus, + spawnLaunchState: effectiveSpawnLaunchState, + spawnRuntimeAlive: effectiveSpawnRuntimeAlive, + spawnEntry, runtimeEntry, }) ? currentTaskCandidate @@ -945,9 +978,10 @@ export const MemberList = memo(function MemberList({ shouldDisplayMemberCurrentTask({ member, isTeamAlive, - spawnStatus: spawnEntry?.status, - spawnLaunchState: spawnEntry?.launchState, - spawnRuntimeAlive: spawnEntry?.runtimeAlive, + spawnStatus: effectiveSpawnStatus, + spawnLaunchState: effectiveSpawnLaunchState, + spawnRuntimeAlive: effectiveSpawnRuntimeAlive, + spawnEntry, runtimeEntry, }) ? reviewCandidate @@ -995,12 +1029,16 @@ export const MemberList = memo(function MemberList({ runtimeSummary={buildRuntimeSummary(member, spawnEntry, runtimeEntry)} runtimeEntry={runtimeEntry} runtimeRunId={runtimeRunId} - spawnStatus={spawnEntry?.status} + spawnStatus={effectiveSpawnStatus} spawnEntry={spawnEntry} - spawnError={spawnEntry?.error ?? spawnEntry?.hardFailureReason} + spawnError={ + canPromoteBootstrapConfirmedVisualState + ? undefined + : (spawnEntry?.error ?? spawnEntry?.hardFailureReason) + } spawnLivenessSource={spawnEntry?.livenessSource} - spawnLaunchState={spawnEntry?.launchState} - spawnRuntimeAlive={spawnEntry?.runtimeAlive} + spawnLaunchState={effectiveSpawnLaunchState} + spawnRuntimeAlive={effectiveSpawnRuntimeAlive} isTeamAlive={isTeamAlive} isTeamProvisioning={isTeamProvisioning} leadActivity={leadActivity} diff --git a/src/renderer/components/team/provisioningSteps.ts b/src/renderer/components/team/provisioningSteps.ts index a8fb62bb..3f34dd2d 100644 --- a/src/renderer/components/team/provisioningSteps.ts +++ b/src/renderer/components/team/provisioningSteps.ts @@ -1,4 +1,10 @@ import { isLeadMember } from '@shared/utils/leadDetection'; +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidence, + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext, + isBootstrapConfirmedProvisionedButNotAliveFailure, + mentionsProcessTableUnavailable, +} from '@shared/utils/teamLaunchFailureReason'; import type { MemberSpawnStatusEntry, @@ -80,6 +86,9 @@ function parseStatusUpdatedAtMs(value: string | undefined): number | null { } function isFailedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean { + if (isBootstrapConfirmedProvisionedButNotAliveFailure(entry)) { + return hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry); + } return entry?.launchState === 'failed_to_start' || entry?.status === 'error'; } @@ -92,13 +101,62 @@ function isStrongRuntimeProcessSpawnEntry(entry: MemberSpawnStatusEntry): boolea } function isConfirmedSpawnEntry(entry: MemberSpawnStatusEntry): boolean { + if (isBootstrapConfirmedProvisionedButNotAliveFailure(entry)) { + return !isFailedSpawnEntry(entry); + } return entry.launchState === 'confirmed_alive' || entry.bootstrapConfirmed === true; } +function spawnEntryContradictsConfirmedJoin(entry: MemberSpawnStatusEntry): boolean { + if (!isConfirmedSpawnEntry(entry) || entry.runtimeAlive !== false) { + return false; + } + if (entry.runtimeDiagnosticSeverity === 'error') { + return true; + } + if ( + entry.livenessKind === 'not_found' || + entry.livenessKind === 'shell_only' || + entry.livenessKind === 'permission_blocked' || + entry.livenessKind === 'runtime_process_candidate' + ) { + return true; + } + const hasProcessTableUnavailableMarker = + mentionsProcessTableUnavailable(entry.runtimeDiagnostic) || + mentionsProcessTableUnavailable(entry.hardFailureReason) || + mentionsProcessTableUnavailable(entry.error); + if (!entry.livenessKind) { + return !hasProcessTableUnavailableMarker; + } + if (entry.livenessKind !== 'registered_only' && entry.livenessKind !== 'stale_metadata') { + return false; + } + return !hasProcessTableUnavailableMarker; +} + function runtimeEntryContradictsConfirmedJoin( + entry: MemberSpawnStatusEntry, runtimeEntry: TeamAgentRuntimeEntry | undefined ): boolean { - return runtimeEntry?.alive === false; + if (runtimeEntry?.alive !== false || runtimeEntry.livenessKind === 'confirmed_bootstrap') { + return false; + } + if ( + isBootstrapConfirmedProvisionedButNotAliveFailure(entry) && + !hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry) && + !hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(entry, runtimeEntry) && + (runtimeEntry.livenessKind == null || + runtimeEntry.livenessKind === 'registered_only' || + runtimeEntry.livenessKind === 'stale_metadata') && + (mentionsProcessTableUnavailable(runtimeEntry.runtimeDiagnostic) || + mentionsProcessTableUnavailable(entry.runtimeDiagnostic) || + mentionsProcessTableUnavailable(entry.hardFailureReason) || + mentionsProcessTableUnavailable(entry.error)) + ) { + return false; + } + return true; } function shouldPreferSnapshotEntryOverLive( @@ -159,7 +217,7 @@ function summarizeLiveLaunchJoinMilestones(params: { continue; } observedTeammateCount += 1; - if (entry.launchState === 'failed_to_start') { + if (isFailedSpawnEntry(entry)) { failedSpawnCount += 1; continue; } @@ -167,14 +225,21 @@ function summarizeLiveLaunchJoinMilestones(params: { skippedSpawnCount += 1; continue; } + if (spawnEntryContradictsConfirmedJoin(entry)) { + pendingSpawnCount += 1; + continue; + } if ( isConfirmedSpawnEntry(entry) && - runtimeEntryContradictsConfirmedJoin(getRuntimeEntry(params.memberRuntimeEntries, memberName)) + runtimeEntryContradictsConfirmedJoin( + entry, + getRuntimeEntry(params.memberRuntimeEntries, memberName) + ) ) { pendingSpawnCount += 1; continue; } - if (entry.launchState === 'confirmed_alive') { + if (isConfirmedSpawnEntry(entry)) { heartbeatConfirmedCount += 1; continue; } diff --git a/src/renderer/components/team/teamRuntimeDisplayRows.ts b/src/renderer/components/team/teamRuntimeDisplayRows.ts index 0737cbf0..65367a15 100644 --- a/src/renderer/components/team/teamRuntimeDisplayRows.ts +++ b/src/renderer/components/team/teamRuntimeDisplayRows.ts @@ -1,3 +1,9 @@ +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidence, + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext, + isBootstrapConfirmedProvisionedButNotAliveFailure, +} from '@shared/utils/teamLaunchFailureReason'; + import type { MemberSpawnStatusEntry, TeamAgentRuntimeDiagnosticSeverity, @@ -139,15 +145,29 @@ function buildRuntimeBackedDisplayRow( spawn?: MemberSpawnStatusEntry ): TeamRuntimeDisplayRow { const hasErrorDiagnostic = runtime.runtimeDiagnosticSeverity === 'error'; + const bootstrapConfirmedProvisionedButNotAlive = + isBootstrapConfirmedProvisionedButNotAliveFailure(spawn); const spawnDegradation = getSpawnDegradation(spawn); + const unsafeRuntimeEvidence = hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext( + spawn, + runtime + ); + const useBootstrapConfirmedState = + bootstrapConfirmedProvisionedButNotAlive && + !hasErrorDiagnostic && + !unsafeRuntimeEvidence && + spawnDegradation == null; const spawnStoppedEvidence = spawnDegradation ? null : getSpawnStoppedEvidence(runtime, spawn); - const state = spawnStoppedEvidence - ? 'stopped' - : getRuntimeBackedState(runtime, hasErrorDiagnostic, spawnDegradation != null); + const state = useBootstrapConfirmedState + ? 'running' + : spawnStoppedEvidence + ? 'stopped' + : getRuntimeBackedState(runtime, hasErrorDiagnostic, spawnDegradation != null); const degradedReason = spawnDegradation ? withLiveProcessContext(spawnDegradation.reason, runtime) : undefined; const stateReason = + (useBootstrapConfirmedState ? 'Bootstrap confirmed' : undefined) ?? degradedReason ?? spawnStoppedEvidence?.reason ?? runtime.runtimeDiagnostic ?? @@ -181,6 +201,17 @@ function buildRuntimeBackedDisplayRow( function getSpawnDegradation(spawn?: MemberSpawnStatusEntry): SpawnDegradation | null { if (!spawn) return null; + if (isBootstrapConfirmedProvisionedButNotAliveFailure(spawn)) { + if (!hasUnsafeProvisionedButNotAliveRuntimeEvidence(spawn)) { + return null; + } + const reason = spawn.runtimeDiagnostic ?? 'Runtime launch status needs attention'; + return { + reason, + diagnostic: spawn.runtimeDiagnostic ?? reason, + diagnosticSeverity: spawn.runtimeDiagnosticSeverity === 'error' ? 'error' : 'warning', + }; + } if (spawn.status === 'error' || spawn.hardFailure === true) { const reason = @@ -226,7 +257,10 @@ function getSpawnStoppedEvidence( runtime: TeamAgentRuntimeEntry, spawn?: MemberSpawnStatusEntry ): SpawnStoppedEvidence | null { - if (!spawn || spawn.runtimeAlive !== false || runtime.livenessKind !== 'confirmed_bootstrap') { + if (isBootstrapConfirmedProvisionedButNotAliveFailure(spawn)) { + return null; + } + if (spawn?.runtimeAlive !== false || runtime.livenessKind !== 'confirmed_bootstrap') { return null; } if (spawn.status !== 'online' && spawn.launchState !== 'confirmed_alive') { @@ -267,6 +301,23 @@ function buildSpawnBackedDisplayRow( memberName: string, spawn: MemberSpawnStatusEntry ): TeamRuntimeDisplayRow { + if ( + isBootstrapConfirmedProvisionedButNotAliveFailure(spawn) && + !hasUnsafeProvisionedButNotAliveRuntimeEvidence(spawn) + ) { + return { + memberName, + state: 'running', + stateReason: 'Bootstrap confirmed', + source: 'spawn-status', + updatedAt: spawn.livenessLastCheckedAt ?? spawn.lastHeartbeatAt ?? spawn.updatedAt, + runtimeModel: spawn.runtimeModel, + diagnostic: spawn.runtimeDiagnostic, + diagnosticSeverity: spawn.runtimeDiagnosticSeverity, + actionsAllowed: false, + }; + } + const spawnDegradation = getSpawnDegradation(spawn); if (spawnDegradation) { return { @@ -359,6 +410,7 @@ function buildSpawnBackedDisplayRow( } function getSpawnOnlyStoppedEvidence(spawn: MemberSpawnStatusEntry): SpawnStoppedEvidence | null { + if (isBootstrapConfirmedProvisionedButNotAliveFailure(spawn)) return null; if (spawn.runtimeAlive !== false) return null; if (spawn.status !== 'online' && spawn.launchState !== 'confirmed_alive') return null; diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 23063056..a2189160 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -4117,6 +4117,27 @@ export const createTeamSlice: StateCreator = (set, operation: 'fetchTeams', }); void get().fetchTeams(); + const terminalRefreshState = get(); + if (isVisibleInActiveTeamSurface(terminalRefreshState, progress.teamName)) { + noteTeamRefreshFanout({ + teamName: progress.teamName, + surface: 'provisioning-progress', + phase: 'scheduled', + reason: terminalReason, + operation: 'fetchMemberSpawnStatuses', + visible: true, + }); + void terminalRefreshState.fetchMemberSpawnStatuses(progress.teamName); + noteTeamRefreshFanout({ + teamName: progress.teamName, + surface: 'provisioning-progress', + phase: 'scheduled', + reason: terminalReason, + operation: 'fetchTeamAgentRuntime', + visible: true, + }); + void terminalRefreshState.fetchTeamAgentRuntime(progress.teamName); + } if (hydratedVisibleTeam) { noteTeamRefreshFanout({ teamName: progress.teamName, diff --git a/src/renderer/utils/memberHelpers.ts b/src/renderer/utils/memberHelpers.ts index 85ea1d2a..2f5b1d00 100644 --- a/src/renderer/utils/memberHelpers.ts +++ b/src/renderer/utils/memberHelpers.ts @@ -1,4 +1,8 @@ import { isLeadMember } from '@shared/utils/leadDetection'; +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext, + isBootstrapConfirmedProvisionedButNotAliveFailure, +} from '@shared/utils/teamLaunchFailureReason'; import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors'; import { @@ -17,6 +21,7 @@ import type { MemberSpawnStatusEntry, MemberStatus, ResolvedTeamMember, + TeamAgentRuntimeDiagnosticSeverity, TeamAgentRuntimeEntry, TeamProviderId, TeamReviewState, @@ -394,7 +399,7 @@ const OPENCODE_SESSION_REFRESH_REASON_PATTERN = /\b(?:resolved_behavior_changed|opencode_app_mcp_transport_changed):[-a-z0-9._~/=]+->[-a-z0-9._~/=]+/i; const OPENCODE_SESSION_REFRESH_FAILURE_PATTERN = // eslint-disable-next-line sonarjs/regex-complexity -- Keyword taxonomy is kept literal to preserve diagnostic behavior. - /(?:^|[_\s:;.\/()-])(?:permission[_\s-]?denied|permission[_\s-]?blocked|access[_\s-]?denied|auth[_\s-]?unavailable|authentication[_\s-]?failed|unauthorized|forbidden|401|403|login[_\s-]?required|not\s+logged\s+in|missing\s+credentials?|invalid\s+credentials?|credentials?[_\s-]?required|credentials?[_\s-]?unavailable|no auth available|authorization|auth(?:entication)?(?:[_\s-]?(?:failed|unavailable))?|invalid api[_\s-]?key|api[_\s-]?key|does not have access|quota|rate[_\s-]?(?:limit|limited)|too many requests|429|model cooldown|cooling down|enospc|no space left|disk is full|capacity exceeded|quota exhausted|usage exceeded|free usage exceeded|key limit exceeded|total limit|insufficient credits|subscribe to go|error|failed|failure|timeout|timed\s+out|network|connection|unable\s+to\s+connect|connect\s+failed|econn[a-z_]*|enotfound|fetch[_\s-]?failed|connection[_\s-]?(?:refused|reset)|aborted|cancel(?:ed|led)|interrupted|service[_\s-]?unavailable|temporarily\s+unavailable|overloaded|visible[_\s-]?reply(?:[_\s-][a-z0-9]+)*|task[_\s-]?refs|relayofmessageid|relay[_\s-]?of[_\s-]?message[_\s-]?id|message[_\s-]?send|non[_\s-]?visible[_\s-]?tool(?:[_\s-][a-z0-9]+)*|protocol[_\s-]?proof)(?=$|[_\s:;.\/(),-])/i; + /(?:^|[_\s:;./()-])(?:permission[_\s-]?denied|permission[_\s-]?blocked|access[_\s-]?denied|auth[_\s-]?unavailable|authentication[_\s-]?failed|unauthorized|forbidden|401|403|login[_\s-]?required|not\s+logged\s+in|missing\s+credentials?|invalid\s+credentials?|credentials?[_\s-]?required|credentials?[_\s-]?unavailable|no auth available|authorization|auth(?:entication)?(?:[_\s-]?(?:failed|unavailable))?|invalid api[_\s-]?key|api[_\s-]?key|does not have access|quota|rate[_\s-]?(?:limit|limited)|too many requests|429|model cooldown|cooling down|enospc|no space left|disk is full|capacity exceeded|quota exhausted|usage exceeded|free usage exceeded|key limit exceeded|total limit|insufficient credits|subscribe to go|error|failed|failure|timeout|timed\s+out|network|connection|unable\s+to\s+connect|connect\s+failed|econn[a-z_]*|enotfound|fetch[_\s-]?failed|connection[_\s-]?(?:refused|reset)|aborted|cancel(?:ed|led)|interrupted|service[_\s-]?unavailable|temporarily\s+unavailable|overloaded|visible[_\s-]?reply(?:[_\s-][a-z0-9]+)*|task[_\s-]?refs|relayofmessageid|relay[_\s-]?of[_\s-]?message[_\s-]?id|message[_\s-]?send|non[_\s-]?visible[_\s-]?tool(?:[_\s-][a-z0-9]+)*|protocol[_\s-]?proof)(?=$|[_\s:;./(),-])/i; const OPENCODE_SESSION_REFRESH_ANY_REASON_PATTERN = /\b(?:resolved_behavior_changed|opencode_app_mcp_transport_changed):[-a-z0-9._~/=]+->[-a-z0-9._~/=]+/gi; const OPENCODE_SESSION_REFRESH_SAFE_MARKER_STATE_PATTERN = @@ -983,6 +988,17 @@ function getCurrentRuntimeOfflineVisualState( return null; } +function hasStoppedRuntimeLivenessKind( + livenessKind: TeamAgentRuntimeEntry['livenessKind'] | undefined +): boolean { + return ( + livenessKind === 'not_found' || + livenessKind === 'registered_only' || + livenessKind === 'shell_only' || + livenessKind === 'stale_metadata' + ); +} + function isCodexNativeProcessTeammate(member: ResolvedTeamMember): boolean { if (isLeadMember(member)) { return false; @@ -1076,6 +1092,7 @@ export function shouldDisplayMemberCurrentTask({ spawnStatus, spawnLaunchState, spawnRuntimeAlive, + spawnEntry, runtimeEntry, }: { member: ResolvedTeamMember; @@ -1083,36 +1100,64 @@ export function shouldDisplayMemberCurrentTask({ spawnStatus?: MemberSpawnStatus; spawnLaunchState?: MemberLaunchState; spawnRuntimeAlive?: boolean; + spawnEntry?: MemberSpawnStatusEntry; runtimeEntry?: TeamAgentRuntimeEntry; }): boolean { + const bootstrapConfirmedProvisionedButNotAlive = + isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry); + const unsafeProvisionedButNotAliveEvidence = + bootstrapConfirmedProvisionedButNotAlive && + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawnEntry, runtimeEntry); + const useBootstrapConfirmedVisualState = + bootstrapConfirmedProvisionedButNotAlive && !unsafeProvisionedButNotAliveEvidence; + const effectiveSpawnStatus = useBootstrapConfirmedVisualState ? 'online' : spawnStatus; + const effectiveSpawnLaunchState = useBootstrapConfirmedVisualState + ? 'confirmed_alive' + : spawnLaunchState; + const effectiveSpawnRuntimeAlive = useBootstrapConfirmedVisualState ? true : spawnRuntimeAlive; if (member.removedAt || member.status === 'terminated') { return false; } if (isTeamAlive === false) { return false; } - if (spawnStatus === 'offline' || spawnStatus === 'error' || spawnStatus === 'skipped') { - return false; - } if ( - spawnLaunchState === 'failed_to_start' || - spawnLaunchState === 'skipped_for_launch' || - spawnLaunchState === 'runtime_pending_permission' + effectiveSpawnStatus === 'offline' || + effectiveSpawnStatus === 'error' || + effectiveSpawnStatus === 'skipped' ) { return false; } if ( - runtimeEntry?.livenessKind === 'shell_only' || - runtimeEntry?.livenessKind === 'registered_only' || - runtimeEntry?.livenessKind === 'stale_metadata' || - runtimeEntry?.livenessKind === 'not_found' + effectiveSpawnLaunchState === 'failed_to_start' || + effectiveSpawnLaunchState === 'skipped_for_launch' || + effectiveSpawnLaunchState === 'runtime_pending_permission' ) { return false; } - if (runtimeEntry?.alive === false) { + if ( + !useBootstrapConfirmedVisualState && + (runtimeEntry?.livenessKind === 'shell_only' || + spawnEntry?.livenessKind === 'shell_only' || + runtimeEntry?.livenessKind === 'registered_only' || + spawnEntry?.livenessKind === 'registered_only' || + runtimeEntry?.livenessKind === 'stale_metadata' || + spawnEntry?.livenessKind === 'stale_metadata' || + runtimeEntry?.livenessKind === 'not_found' || + spawnEntry?.livenessKind === 'not_found') + ) { return false; } - if (spawnRuntimeAlive === false) { + if (runtimeEntry?.runtimeDiagnosticSeverity === 'error') { + return false; + } + if (spawnEntry?.runtimeDiagnosticSeverity === 'error') { + return false; + } + if (runtimeEntry?.alive === false && !useBootstrapConfirmedVisualState) { + return false; + } + if (effectiveSpawnRuntimeAlive === false) { return false; } if (isCodexNativeProcessTeammate(member) && !hasLiveRuntimeProcessEvidence(runtimeEntry)) { @@ -1228,6 +1273,9 @@ export function isOpenCodeRelaunchActionable({ runtimeEntry?.livenessKind === 'stale_metadata' ); } + if (isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry)) { + return hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawnEntry, runtimeEntry); + } if ( spawnEntry?.launchState === 'failed_to_start' || spawnEntry?.launchState === 'skipped_for_launch' || @@ -1280,7 +1328,11 @@ export function buildMemberLaunchPresentation({ spawnBootstrapStalled, spawnAgentToolAccepted, spawnHardFailure, + spawnHardFailureReason, + spawnError, + spawnRuntimeDiagnostic, spawnLivenessKind, + spawnRuntimeDiagnosticSeverity, spawnFirstSpawnAcceptedAt, spawnUpdatedAt, runtimeAdvisory, @@ -1300,7 +1352,11 @@ export function buildMemberLaunchPresentation({ spawnBootstrapStalled?: boolean; spawnAgentToolAccepted?: boolean; spawnHardFailure?: boolean; + spawnHardFailureReason?: string; + spawnError?: string; + spawnRuntimeDiagnostic?: string; spawnLivenessKind?: TeamAgentRuntimeEntry['livenessKind']; + spawnRuntimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity; spawnFirstSpawnAcceptedAt?: string; spawnUpdatedAt?: string; runtimeAdvisory: MemberRuntimeAdvisory | undefined; @@ -1311,46 +1367,105 @@ export function buildMemberLaunchPresentation({ leadActivity?: LeadActivityState; nowMs?: number; }): MemberLaunchPresentation { + const bootstrapConfirmedProvisionedButNotAlive = + isBootstrapConfirmedProvisionedButNotAliveFailure({ + status: spawnStatus, + launchState: spawnLaunchState, + hardFailure: spawnHardFailure, + hardFailureReason: spawnHardFailureReason, + error: spawnError, + runtimeDiagnostic: spawnRuntimeDiagnostic, + runtimeDiagnosticSeverity: spawnRuntimeDiagnosticSeverity, + bootstrapConfirmed: spawnBootstrapConfirmed, + livenessKind: spawnLivenessKind ?? runtimeEntry?.livenessKind, + }); + const hasSpawnRuntimeErrorDiagnostic = spawnRuntimeDiagnosticSeverity === 'error'; + const hasRuntimeErrorDiagnostic = runtimeEntry?.runtimeDiagnosticSeverity === 'error'; + const hasUnsafeProvisionedButNotAliveEvidence = + bootstrapConfirmedProvisionedButNotAlive && + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext( + { + status: spawnStatus, + launchState: spawnLaunchState, + hardFailure: spawnHardFailure, + hardFailureReason: spawnHardFailureReason, + error: spawnError, + runtimeDiagnostic: spawnRuntimeDiagnostic, + runtimeDiagnosticSeverity: spawnRuntimeDiagnosticSeverity, + bootstrapConfirmed: spawnBootstrapConfirmed, + livenessKind: spawnLivenessKind, + }, + runtimeEntry + ); + const allowBootstrapConfirmedVisualPromotion = + bootstrapConfirmedProvisionedButNotAlive && + !hasSpawnRuntimeErrorDiagnostic && + !hasRuntimeErrorDiagnostic && + !hasUnsafeProvisionedButNotAliveEvidence; + const useBootstrapConfirmedRuntimeAlive = + allowBootstrapConfirmedVisualPromotion && !hasRuntimeErrorDiagnostic; + const suppressConfirmedLaunchRuntimeAlivePromotion = + bootstrapConfirmedProvisionedButNotAlive && !useBootstrapConfirmedRuntimeAlive; + const visualSpawnStatus = allowBootstrapConfirmedVisualPromotion ? 'online' : spawnStatus; + const visualSpawnLaunchState = allowBootstrapConfirmedVisualPromotion + ? 'confirmed_alive' + : spawnLaunchState; + const visualSpawnRuntimeAlive = useBootstrapConfirmedRuntimeAlive ? true : spawnRuntimeAlive; + const visualSpawnBootstrapConfirmed = allowBootstrapConfirmedVisualPromotion + ? true + : spawnBootstrapConfirmed; + const visualSpawnHardFailure = allowBootstrapConfirmedVisualPromotion ? false : spawnHardFailure; + const visualSpawnLivenessKind = allowBootstrapConfirmedVisualPromotion + ? 'confirmed_bootstrap' + : spawnLivenessKind; + const visualRuntimeEntry = + useBootstrapConfirmedRuntimeAlive && runtimeEntry + ? ({ + ...runtimeEntry, + alive: true, + livenessKind: 'confirmed_bootstrap', + } satisfies TeamAgentRuntimeEntry) + : runtimeEntry; const currentRuntimeOfflineVisualState = getCurrentRuntimeOfflineVisualState( member, - runtimeEntry, - spawnStatus, - spawnLaunchState, - spawnRuntimeAlive, - spawnBootstrapConfirmed, + visualRuntimeEntry, + visualSpawnStatus, + visualSpawnLaunchState, + visualSpawnRuntimeAlive, + visualSpawnBootstrapConfirmed, isTeamProvisioning ); const hasConfirmedSpawnLaunch = - spawnLaunchState === 'confirmed_alive' && spawnBootstrapConfirmed === true; + visualSpawnLaunchState === 'confirmed_alive' && visualSpawnBootstrapConfirmed === true; const suppressOpenCodeAppMcpAdvisory = isHealthyOpenCodeAppMcpConnectivityAdvisory({ providerId: member.providerId, runtimeAdvisory, - spawnStatus, - launchState: spawnLaunchState, - runtimeAlive: spawnRuntimeAlive, - bootstrapConfirmed: spawnBootstrapConfirmed, + spawnStatus: visualSpawnStatus, + launchState: visualSpawnLaunchState, + runtimeAlive: visualSpawnRuntimeAlive, + bootstrapConfirmed: visualSpawnBootstrapConfirmed, agentToolAccepted: spawnAgentToolAccepted, - hardFailure: spawnHardFailure, - livenessKind: spawnLivenessKind ?? runtimeEntry?.livenessKind, - runtimeEntry, + hardFailure: visualSpawnHardFailure, + livenessKind: visualSpawnLivenessKind ?? visualRuntimeEntry?.livenessKind, + runtimeEntry: visualRuntimeEntry, }); const displayRuntimeAdvisory = suppressOpenCodeAppMcpAdvisory ? undefined : runtimeAdvisory; const effectiveSpawnStatus = hasConfirmedSpawnLaunch && currentRuntimeOfflineVisualState == null && - (spawnStatus === 'waiting' || spawnStatus === 'spawning') + (visualSpawnStatus === 'waiting' || visualSpawnStatus === 'spawning') ? 'online' - : spawnStatus; + : visualSpawnStatus; const effectiveSpawnRuntimeAlive = currentRuntimeOfflineVisualState != null ? false - : hasConfirmedSpawnLaunch + : hasConfirmedSpawnLaunch && !suppressConfirmedLaunchRuntimeAlivePromotion ? true - : spawnRuntimeAlive; + : visualSpawnRuntimeAlive; const presenceLabel = getLaunchAwarePresenceLabel( member, effectiveSpawnStatus, - spawnLaunchState, + visualSpawnLaunchState, spawnLivenessSource, effectiveSpawnRuntimeAlive, displayRuntimeAdvisory, @@ -1362,7 +1477,7 @@ export function buildMemberLaunchPresentation({ const baseDotClass = getSpawnAwareDotClass( member, effectiveSpawnStatus, - spawnLaunchState, + visualSpawnLaunchState, effectiveSpawnRuntimeAlive, isLaunchSettling, isTeamAlive, @@ -1371,7 +1486,7 @@ export function buildMemberLaunchPresentation({ ); const cardClass = getSpawnCardClass( effectiveSpawnStatus, - spawnLaunchState, + visualSpawnLaunchState, effectiveSpawnRuntimeAlive, isLaunchSettling, isTeamAlive, @@ -1393,8 +1508,8 @@ export function buildMemberLaunchPresentation({ const startingIsStale = !hasConfirmedSpawnLaunch && isMemberStartingStale({ - spawnStatus, - spawnLaunchState, + spawnStatus: visualSpawnStatus, + spawnLaunchState: visualSpawnLaunchState, spawnFirstSpawnAcceptedAt, spawnUpdatedAt, nowMs, @@ -1402,19 +1517,19 @@ export function buildMemberLaunchPresentation({ let launchVisualState: MemberLaunchVisualState = null; if (isTeamAlive !== false || isTeamProvisioning) { - if (spawnLaunchState === 'failed_to_start' || spawnStatus === 'error') { + if (visualSpawnLaunchState === 'failed_to_start' || visualSpawnStatus === 'error') { launchVisualState = 'error'; - } else if (spawnLaunchState === 'skipped_for_launch' || spawnStatus === 'skipped') { + } else if (visualSpawnLaunchState === 'skipped_for_launch' || visualSpawnStatus === 'skipped') { launchVisualState = 'skipped'; - } else if (spawnLaunchState === 'runtime_pending_permission') { + } else if (visualSpawnLaunchState === 'runtime_pending_permission') { launchVisualState = 'permission_pending'; } else if (spawnBootstrapStalled === true) { launchVisualState = 'bootstrap_stalled'; } else if (currentRuntimeOfflineVisualState != null) { launchVisualState = currentRuntimeOfflineVisualState; - } else if (runtimeEntry?.livenessKind === 'shell_only') { + } else if (visualRuntimeEntry?.livenessKind === 'shell_only') { launchVisualState = 'shell_only'; - } else if (runtimeEntry?.livenessKind === 'runtime_process_candidate') { + } else if (visualRuntimeEntry?.livenessKind === 'runtime_process_candidate') { launchVisualState = 'runtime_candidate'; } else if (!hasConfirmedSpawnLaunch && startingIsStale) { launchVisualState = 'starting_stale'; @@ -1422,9 +1537,9 @@ export function buildMemberLaunchPresentation({ !hasConfirmedSpawnLaunch && isQueuedOpenCodeLaunch( member, - spawnStatus, - spawnLaunchState, - runtimeEntry, + visualSpawnStatus, + visualSpawnLaunchState, + visualRuntimeEntry, isLaunchSettling, isTeamProvisioning ) @@ -1433,21 +1548,21 @@ export function buildMemberLaunchPresentation({ } else if ( !hasConfirmedSpawnLaunch && isLaunchStillStarting( - spawnStatus, - spawnLaunchState, - spawnRuntimeAlive, + visualSpawnStatus, + visualSpawnLaunchState, + visualSpawnRuntimeAlive, keepLaunchSettlingVisuals ) ) { - launchVisualState = spawnStatus === 'spawning' ? 'spawning' : 'waiting'; + launchVisualState = visualSpawnStatus === 'spawning' ? 'spawning' : 'waiting'; } else if ( !hasConfirmedSpawnLaunch && - spawnLaunchState === 'runtime_pending_bootstrap' && - (runtimeEntry?.livenessKind === 'runtime_process' || - (spawnStatus === 'online' && spawnRuntimeAlive === true)) + visualSpawnLaunchState === 'runtime_pending_bootstrap' && + (visualRuntimeEntry?.livenessKind === 'runtime_process' || + (visualSpawnStatus === 'online' && visualSpawnRuntimeAlive === true)) ) { launchVisualState = 'runtime_pending'; - } else if (isLaunchSettling && spawnLaunchState === 'confirmed_alive') { + } else if (isLaunchSettling && visualSpawnLaunchState === 'confirmed_alive') { launchVisualState = 'settling'; } } @@ -1471,12 +1586,12 @@ export function buildMemberLaunchPresentation({ ? (launchStatusLabel ?? presenceLabel) : presenceLabel; const spawnBadgeLabel = - spawnStatus && spawnStatus !== 'online' - ? spawnStatus === 'waiting' || spawnStatus === 'spawning' + effectiveSpawnStatus && effectiveSpawnStatus !== 'online' + ? effectiveSpawnStatus === 'waiting' || effectiveSpawnStatus === 'spawning' ? startingIsStale ? 'starting stale' : 'starting' - : spawnStatus + : effectiveSpawnStatus : null; return { diff --git a/src/renderer/utils/memberLaunchDiagnostics.ts b/src/renderer/utils/memberLaunchDiagnostics.ts index fe316b72..e4efbeeb 100644 --- a/src/renderer/utils/memberLaunchDiagnostics.ts +++ b/src/renderer/utils/memberLaunchDiagnostics.ts @@ -1,3 +1,9 @@ +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidence, + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext, + isBootstrapConfirmedProvisionedButNotAliveFailure, +} from '@shared/utils/teamLaunchFailureReason'; + import { isHealthyOpenCodeAppMcpConnectivityAdvisory } from './openCodeAdvisoryHealth'; import type { @@ -87,6 +93,15 @@ const SECRET_ENV_KEY_PARTS = [ 'PASSWORD', 'AUTHORIZATION', ]; + +function hasStoppedRuntimeLivenessKind(livenessKind: TeamAgentRuntimeLivenessKind | undefined) { + return ( + livenessKind === 'not_found' || + livenessKind === 'registered_only' || + livenessKind === 'shell_only' || + livenessKind === 'stale_metadata' + ); +} const OPENCODE_SESSION_REFRESH_REASON_MARKERS = [ 'resolved_behavior_changed', 'opencode_app_mcp_transport_changed', @@ -94,7 +109,7 @@ const OPENCODE_SESSION_REFRESH_REASON_MARKERS = [ const OPENCODE_SESSION_REFRESH_REASON_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789._~/=->'; const OPENCODE_SESSION_REFRESH_FAILURE_PATTERN = // eslint-disable-next-line sonarjs/regex-complexity -- Keyword taxonomy is kept literal to preserve diagnostic behavior. - /(?:^|[_\s:;.\/()-])(?:permission[_\s-]?denied|permission[_\s-]?blocked|access[_\s-]?denied|auth[_\s-]?unavailable|authentication[_\s-]?failed|unauthorized|forbidden|401|403|login[_\s-]?required|not\s+logged\s+in|missing\s+credentials?|invalid\s+credentials?|credentials?[_\s-]?required|credentials?[_\s-]?unavailable|no auth available|authorization|auth(?:entication)?(?:[_\s-]?(?:failed|unavailable))?|invalid api[_\s-]?key|api[_\s-]?key|does not have access|quota|rate[_\s-]?(?:limit|limited)|too many requests|429|model cooldown|cooling down|enospc|no space left|disk is full|capacity exceeded|quota exhausted|usage exceeded|free usage exceeded|key limit exceeded|total limit|insufficient credits|subscribe to go|error|failed|failure|timeout|timed\s+out|network|connection|unable\s+to\s+connect|connect\s+failed|econn[a-z_]*|enotfound|fetch[_\s-]?failed|connection[_\s-]?(?:refused|reset)|aborted|cancel(?:ed|led)|interrupted|service[_\s-]?unavailable|temporarily\s+unavailable|overloaded|visible[_\s-]?reply(?:[_\s-][a-z0-9]+)*|task[_\s-]?refs|relayofmessageid|relay[_\s-]?of[_\s-]?message[_\s-]?id|message[_\s-]?send|non[_\s-]?visible[_\s-]?tool(?:[_\s-][a-z0-9]+)*|protocol[_\s-]?proof)(?=$|[_\s:;.\/(),-])/i; + /(?:^|[_\s:;./()-])(?:permission[_\s-]?denied|permission[_\s-]?blocked|access[_\s-]?denied|auth[_\s-]?unavailable|authentication[_\s-]?failed|unauthorized|forbidden|401|403|login[_\s-]?required|not\s+logged\s+in|missing\s+credentials?|invalid\s+credentials?|credentials?[_\s-]?required|credentials?[_\s-]?unavailable|no auth available|authorization|auth(?:entication)?(?:[_\s-]?(?:failed|unavailable))?|invalid api[_\s-]?key|api[_\s-]?key|does not have access|quota|rate[_\s-]?(?:limit|limited)|too many requests|429|model cooldown|cooling down|enospc|no space left|disk is full|capacity exceeded|quota exhausted|usage exceeded|free usage exceeded|key limit exceeded|total limit|insufficient credits|subscribe to go|error|failed|failure|timeout|timed\s+out|network|connection|unable\s+to\s+connect|connect\s+failed|econn[a-z_]*|enotfound|fetch[_\s-]?failed|connection[_\s-]?(?:refused|reset)|aborted|cancel(?:ed|led)|interrupted|service[_\s-]?unavailable|temporarily\s+unavailable|overloaded|visible[_\s-]?reply(?:[_\s-][a-z0-9]+)*|task[_\s-]?refs|relayofmessageid|relay[_\s-]?of[_\s-]?message[_\s-]?id|message[_\s-]?send|non[_\s-]?visible[_\s-]?tool(?:[_\s-][a-z0-9]+)*|protocol[_\s-]?proof)(?=$|[_\s:;./(),-])/i; const OPENCODE_SESSION_REFRESH_SAFE_MARKER_STATE_PATTERN = /\b(?:not_observed|pending|prompt_not_indexed|responded_tool_call|responded_visible_message|responded_non_visible_tool|responded_plain_text|permission_blocked|tool_error|empty_assistant_turn|prompt_delivered_no_assistant_message|session_stale|session_error|reconcile_failed)\b/g; @@ -527,9 +542,48 @@ export function buildMemberLaunchDiagnosticsPayload(params: { const providerBackendId = runtimeEntry?.providerBackendId ?? params.member?.providerBackendId; const laneId = runtimeEntry?.laneId ?? params.member?.laneId; const laneKind = runtimeEntry?.laneKind ?? params.member?.laneKind; - const livenessKind = spawnEntry?.livenessKind ?? runtimeEntry?.livenessKind; - const launchState = spawnEntry?.launchState ?? params.launchState; - const spawnStatus = spawnEntry?.status ?? params.spawnStatus; + const livenessKind = hasStoppedRuntimeLivenessKind(runtimeEntry?.livenessKind) + ? runtimeEntry?.livenessKind + : (spawnEntry?.livenessKind ?? runtimeEntry?.livenessKind); + const bootstrapConfirmedProvisionedButNotAlive = + isBootstrapConfirmedProvisionedButNotAliveFailure(spawnEntry); + const hasUnsafeSpawnProvisionedButNotAliveEvidence = + bootstrapConfirmedProvisionedButNotAlive && + hasUnsafeProvisionedButNotAliveRuntimeEvidence(spawnEntry); + const hasUnsafeRuntimeProvisionedButNotAliveEvidence = + bootstrapConfirmedProvisionedButNotAlive && + !hasUnsafeSpawnProvisionedButNotAliveEvidence && + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext(spawnEntry, runtimeEntry); + const hasUnsafeProvisionedButNotAliveEvidence = + bootstrapConfirmedProvisionedButNotAlive && + (hasUnsafeSpawnProvisionedButNotAliveEvidence || + hasUnsafeRuntimeProvisionedButNotAliveEvidence); + const useBootstrapConfirmedVisualState = + bootstrapConfirmedProvisionedButNotAlive && + spawnEntry?.runtimeDiagnosticSeverity !== 'error' && + runtimeEntry?.runtimeDiagnosticSeverity !== 'error' && + !hasUnsafeProvisionedButNotAliveEvidence; + const useBootstrapConfirmedRuntimeAlive = + useBootstrapConfirmedVisualState && + runtimeEntry?.runtimeDiagnosticSeverity !== 'error' && + spawnEntry?.runtimeDiagnosticSeverity !== 'error'; + const runtimeEntryDiagnostic = boundedString(runtimeEntry?.runtimeDiagnostic); + const hasRuntimeDiagnosticEvidence = + runtimeEntryDiagnostic != null || runtimeEntry?.runtimeDiagnosticSeverity != null; + const useSpawnDiagnosticsForHealedEntry = + bootstrapConfirmedProvisionedButNotAlive && !hasRuntimeDiagnosticEvidence; + const keepSpawnFailureDiagnostics = + useSpawnDiagnosticsForHealedEntry || + hasUnsafeSpawnProvisionedButNotAliveEvidence || + spawnEntry?.runtimeDiagnosticSeverity === 'error'; + const launchState = useBootstrapConfirmedVisualState + ? 'confirmed_alive' + : (spawnEntry?.launchState ?? params.launchState); + const spawnStatus = useBootstrapConfirmedVisualState + ? 'online' + : (spawnEntry?.status ?? params.spawnStatus); + const spawnRuntimeAlive = useBootstrapConfirmedRuntimeAlive ? true : spawnEntry?.runtimeAlive; + const spawnHardFailure = useBootstrapConfirmedVisualState ? false : spawnEntry?.hardFailure; const runtimeAdvisoryTitle = boundedString(params.runtimeAdvisoryTitle); const runtimeAdvisoryLabel = boundedString(params.runtimeAdvisoryLabel ?? undefined); const runtimeAdvisoryMessage = boundedString(runtimeAdvisory?.message); @@ -541,10 +595,10 @@ export function buildMemberLaunchDiagnosticsPayload(params: { runtimeAdvisoryMessage, spawnStatus, launchState, - runtimeAlive: spawnEntry?.runtimeAlive, + runtimeAlive: spawnRuntimeAlive, bootstrapConfirmed: spawnEntry?.bootstrapConfirmed, agentToolAccepted: spawnEntry?.agentToolAccepted, - hardFailure: spawnEntry?.hardFailure, + hardFailure: spawnHardFailure, livenessKind, runtimeEntry, }); @@ -553,9 +607,17 @@ export function buildMemberLaunchDiagnosticsPayload(params: { ? (runtimeAdvisoryTitle ?? runtimeAdvisoryLabel ?? runtimeAdvisoryMessage) : undefined; const runtimeDiagnosticSeverity = - spawnEntry?.runtimeDiagnosticSeverity ?? runtimeEntry?.runtimeDiagnosticSeverity; + spawnEntry?.runtimeDiagnosticSeverity === 'error' + ? spawnEntry.runtimeDiagnosticSeverity + : bootstrapConfirmedProvisionedButNotAlive + ? (runtimeEntry?.runtimeDiagnosticSeverity ?? + (useSpawnDiagnosticsForHealedEntry ? spawnEntry?.runtimeDiagnosticSeverity : undefined)) + : (spawnEntry?.runtimeDiagnosticSeverity ?? runtimeEntry?.runtimeDiagnosticSeverity); const spawnRuntimeDiagnosticCardError = isRuntimeDiagnosticCardError({ - runtimeDiagnostic: spawnEntry?.runtimeDiagnostic, + runtimeDiagnostic: + bootstrapConfirmedProvisionedButNotAlive && !keepSpawnFailureDiagnostics + ? undefined + : spawnEntry?.runtimeDiagnostic, runtimeDiagnosticSeverity: spawnEntry?.runtimeDiagnosticSeverity, launchState: spawnEntry?.launchState, spawnStatus: spawnEntry?.status, @@ -564,6 +626,10 @@ export function buildMemberLaunchDiagnosticsPayload(params: { }) ? spawnEntry?.runtimeDiagnostic : undefined; + const healedSpawnFailureCardError = + keepSpawnFailureDiagnostics && spawnEntry?.runtimeDiagnosticSeverity === 'error' + ? (spawnRuntimeDiagnosticCardError ?? spawnEntry?.error ?? spawnEntry?.hardFailureReason) + : undefined; const runtimeEntryDiagnosticCardError = isRuntimeDiagnosticCardError({ runtimeDiagnostic: runtimeEntry?.runtimeDiagnostic, runtimeDiagnosticSeverity: runtimeEntry?.runtimeDiagnosticSeverity, @@ -572,19 +638,24 @@ export function buildMemberLaunchDiagnosticsPayload(params: { ? runtimeEntry?.runtimeDiagnostic : undefined; const runtimeDiagnostic = - boundedString(spawnEntry?.runtimeDiagnostic) ?? - boundedString(runtimeEntry?.runtimeDiagnostic) ?? - boundedString(spawnEntry?.hardFailureReason) ?? - boundedString(spawnEntry?.error) ?? + (bootstrapConfirmedProvisionedButNotAlive && !keepSpawnFailureDiagnostics + ? undefined + : boundedString(spawnEntry?.runtimeDiagnostic)) ?? + runtimeEntryDiagnostic ?? + (bootstrapConfirmedProvisionedButNotAlive && !keepSpawnFailureDiagnostics + ? undefined + : (boundedString(spawnEntry?.hardFailureReason) ?? boundedString(spawnEntry?.error))) ?? runtimeAdvisoryMessage; const memberCardError = firstMemberCardFailureReason({ - candidates: [ - spawnEntry?.error, - spawnEntry?.hardFailureReason, - spawnRuntimeDiagnosticCardError, - runtimeEntryDiagnosticCardError, - runtimeAdvisoryCardError, - ], + candidates: bootstrapConfirmedProvisionedButNotAlive + ? [healedSpawnFailureCardError, runtimeEntryDiagnosticCardError, runtimeAdvisoryCardError] + : [ + spawnEntry?.error, + spawnEntry?.hardFailureReason, + spawnRuntimeDiagnosticCardError, + runtimeEntryDiagnosticCardError, + runtimeAdvisoryCardError, + ], evidence: [ spawnEntry?.runtimeDiagnostic, runtimeEntry?.runtimeDiagnostic, @@ -601,8 +672,13 @@ export function buildMemberLaunchDiagnosticsPayload(params: { runtimeAdvisoryTitle ? [runtimeAdvisoryTitle] : undefined, runtimeAdvisoryLabel ? [runtimeAdvisoryLabel] : undefined, runtimeAdvisoryMessage ? [runtimeAdvisoryMessage] : undefined, - spawnEntry?.hardFailureReason ? [spawnEntry.hardFailureReason] : undefined, - spawnEntry?.error ? [spawnEntry.error] : undefined, + (!bootstrapConfirmedProvisionedButNotAlive || keepSpawnFailureDiagnostics) && + spawnEntry?.hardFailureReason + ? [spawnEntry.hardFailureReason] + : undefined, + (!bootstrapConfirmedProvisionedButNotAlive || keepSpawnFailureDiagnostics) && spawnEntry?.error + ? [spawnEntry.error] + : undefined, runtimeEntry?.diagnostics ); const runId = boundedString(params.runId ?? undefined); @@ -648,18 +724,14 @@ export function buildMemberLaunchDiagnosticsPayload(params: { ...(typeof runtimeEntry?.restartable === 'boolean' ? { restartable: runtimeEntry.restartable } : {}), - ...(typeof spawnEntry?.runtimeAlive === 'boolean' - ? { runtimeAlive: spawnEntry.runtimeAlive } - : {}), + ...(typeof spawnRuntimeAlive === 'boolean' ? { runtimeAlive: spawnRuntimeAlive } : {}), ...(typeof spawnEntry?.bootstrapConfirmed === 'boolean' ? { bootstrapConfirmed: spawnEntry.bootstrapConfirmed } : {}), ...(typeof spawnEntry?.agentToolAccepted === 'boolean' ? { agentToolAccepted: spawnEntry.agentToolAccepted } : {}), - ...(typeof spawnEntry?.hardFailure === 'boolean' - ? { hardFailure: spawnEntry.hardFailure } - : {}), + ...(typeof spawnHardFailure === 'boolean' ? { hardFailure: spawnHardFailure } : {}), ...(livenessKind ? { livenessKind } : {}), ...((spawnEntry?.livenessSource ?? params.livenessSource) ? { livenessSource: spawnEntry?.livenessSource ?? params.livenessSource } @@ -751,6 +823,9 @@ function parseStatusUpdatedAtMs(value: string | undefined): number | null { } function isFailedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean { + if (isBootstrapConfirmedProvisionedButNotAliveFailure(entry)) { + return hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry); + } return entry?.launchState === 'failed_to_start' || entry?.status === 'error'; } diff --git a/src/renderer/utils/teamProvisioningPresentation.ts b/src/renderer/utils/teamProvisioningPresentation.ts index f246a707..94169385 100644 --- a/src/renderer/utils/teamProvisioningPresentation.ts +++ b/src/renderer/utils/teamProvisioningPresentation.ts @@ -5,6 +5,10 @@ import { getLaunchJoinState, } from '@renderer/components/team/provisioningSteps'; import { isLeadMember } from '@shared/utils/leadDetection'; +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidence, + isBootstrapConfirmedProvisionedButNotAliveFailure, +} from '@shared/utils/teamLaunchFailureReason'; import type { MemberSpawnStatusEntry, @@ -85,9 +89,19 @@ function parseStatusUpdatedAtMs(value: string | undefined): number | null { } function isFailedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean { + if (isBootstrapConfirmedProvisionedButNotAliveFailure(entry)) { + return hasUnsafeProvisionedButNotAliveRuntimeEvidence(entry); + } return entry?.launchState === 'failed_to_start' || entry?.status === 'error'; } +function isConfirmedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean { + if (isBootstrapConfirmedProvisionedButNotAliveFailure(entry)) { + return !isFailedSpawnEntry(entry); + } + return entry?.launchState === 'confirmed_alive' || entry?.bootstrapConfirmed === true; +} + function isSkippedSpawnEntry(entry: MemberSpawnStatusEntry | undefined): boolean { return entry?.launchState === 'skipped_for_launch' || entry?.skippedForLaunch === true; } @@ -125,7 +139,7 @@ function isOpenCodeSecondaryRetryCandidate(params: { ) { return false; } - return entry.launchState === 'failed_to_start' || entry.status === 'error'; + return isFailedSpawnEntry(entry); } function shouldPreferSnapshotEntryOverLive(params: { @@ -275,7 +289,7 @@ function getPendingDiagnosticNameGroups(params: { }); if ( !entry || - entry.launchState === 'confirmed_alive' || + isConfirmedSpawnEntry(entry) || isFailedSpawnEntry(entry) || isSkippedSpawnEntry(entry) ) { @@ -328,7 +342,7 @@ function getPendingSpawnNames(params: { }); return ( entry != null && - entry.launchState !== 'confirmed_alive' && + !isConfirmedSpawnEntry(entry) && !isFailedSpawnEntry(entry) && !isSkippedSpawnEntry(entry) ); @@ -611,9 +625,7 @@ function getFailedSpawnDetails(params: { }), ] as const; }) - .filter( - ([, entry]) => entry && (entry.launchState === 'failed_to_start' || entry.status === 'error') - ) + .filter(([, entry]) => isFailedSpawnEntry(entry)) .map(([name, entry]) => ({ name, reason: diff --git a/src/shared/utils/teamLaunchFailureReason.ts b/src/shared/utils/teamLaunchFailureReason.ts new file mode 100644 index 00000000..fdac36f5 --- /dev/null +++ b/src/shared/utils/teamLaunchFailureReason.ts @@ -0,0 +1,142 @@ +import type { + MemberLaunchState, + MemberSpawnStatus, + TeamAgentRuntimeDiagnosticSeverity, + TeamAgentRuntimeLivenessKind, +} from '@shared/types'; + +export interface ProvisionedButNotAliveLaunchEntry { + launchState?: MemberLaunchState; + status?: MemberSpawnStatus; + hardFailure?: boolean; + hardFailureReason?: string; + error?: string; + runtimeDiagnostic?: string; + runtimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity; + bootstrapConfirmed?: boolean; + livenessKind?: TeamAgentRuntimeLivenessKind; +} + +export function stripProcessTableUnavailableDiagnosticSuffix(reason: string): string | null { + const match = /^(.*?);\s*process table (?:is )?unavailable$/i.exec(reason.trim()); + const baseReason = match?.[1]?.trim(); + return baseReason && baseReason.length > 0 ? baseReason : null; +} + +export function isProvisionedButNotAliveFailureReason(reason?: string): boolean { + return isCliProvisionedButNotAliveFailureReason(reason); +} + +export function isCliProvisionedButNotAliveFailureReason(reason?: string): boolean { + const text = reason?.trim(); + if (!text) { + return false; + } + const normalizedText = stripProcessTableUnavailableDiagnosticSuffix(text) ?? text; + return /^CLI process exited \(code (?:unknown|-?\d+|\?)\)\s+[-\u2013\u2014]\s+team provisioned but not alive$/i.test( + normalizedText + ); +} + +export function mentionsProcessTableUnavailable(value: string | undefined): boolean { + return /\bprocess table\b.*\bunavailable\b/i.test(value ?? ''); +} + +export function hasBootstrapConfirmationProofForLaunchFailure( + entry: ProvisionedButNotAliveLaunchEntry | undefined +): boolean { + return ( + entry?.bootstrapConfirmed === true || + entry?.launchState === 'confirmed_alive' || + entry?.livenessKind === 'confirmed_bootstrap' + ); +} + +export function isProvisionedButNotAliveLaunchFailure( + entry: ProvisionedButNotAliveLaunchEntry | undefined +): boolean { + if (!entry) { + return false; + } + const hardFailureReason = entry.hardFailureReason?.trim(); + const failureReasonMatches = hardFailureReason + ? isProvisionedButNotAliveFailureReason(hardFailureReason) + : isProvisionedButNotAliveFailureReason(entry.error ?? entry.runtimeDiagnostic); + if (!failureReasonMatches) { + return false; + } + return ( + entry.launchState === 'failed_to_start' || + entry.status === 'error' || + entry.hardFailure === true + ); +} + +export function isBootstrapConfirmedProvisionedButNotAliveFailure( + entry: ProvisionedButNotAliveLaunchEntry | undefined +): boolean { + return ( + isProvisionedButNotAliveLaunchFailure(entry) && + hasBootstrapConfirmationProofForLaunchFailure(entry) + ); +} + +export function hasUnsafeProvisionedButNotAliveRuntimeEvidence( + entry: ProvisionedButNotAliveLaunchEntry | undefined +): boolean { + if (!entry) { + return false; + } + if (entry.runtimeDiagnosticSeverity === 'error') { + return true; + } + if ( + entry.livenessKind === 'not_found' || + entry.livenessKind === 'shell_only' || + entry.livenessKind === 'permission_blocked' || + entry.livenessKind === 'runtime_process_candidate' + ) { + return true; + } + const hasProcessTableUnavailableMarker = + mentionsProcessTableUnavailable(entry.runtimeDiagnostic) || + mentionsProcessTableUnavailable(entry.hardFailureReason) || + mentionsProcessTableUnavailable(entry.error); + if (!entry.livenessKind) { + return !hasProcessTableUnavailableMarker; + } + if (entry.livenessKind !== 'registered_only' && entry.livenessKind !== 'stale_metadata') { + return false; + } + return !hasProcessTableUnavailableMarker; +} + +export function hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext( + spawnEntry: ProvisionedButNotAliveLaunchEntry | undefined, + runtimeEntry: ProvisionedButNotAliveLaunchEntry | undefined +): boolean { + if (hasUnsafeProvisionedButNotAliveRuntimeEvidence(spawnEntry)) { + return true; + } + if (!runtimeEntry) { + return false; + } + + const runtimeDiagnostic = runtimeEntry.runtimeDiagnostic?.trim(); + if ( + !runtimeDiagnostic && + (runtimeEntry.livenessKind == null || + runtimeEntry.livenessKind === 'registered_only' || + runtimeEntry.livenessKind === 'stale_metadata') + ) { + return hasUnsafeProvisionedButNotAliveRuntimeEvidence({ + runtimeDiagnostic: spawnEntry?.runtimeDiagnostic, + hardFailureReason: spawnEntry?.hardFailureReason, + error: spawnEntry?.error, + runtimeDiagnosticSeverity: runtimeEntry.runtimeDiagnosticSeverity, + livenessKind: runtimeEntry.livenessKind, + }); + } + + return hasUnsafeProvisionedButNotAliveRuntimeEvidence(runtimeEntry); +} diff --git a/test/main/services/team/TeamConfigReader.test.ts b/test/main/services/team/TeamConfigReader.test.ts index 31fb3fca..86d598b0 100644 --- a/test/main/services/team/TeamConfigReader.test.ts +++ b/test/main/services/team/TeamConfigReader.test.ts @@ -151,6 +151,74 @@ describe('TeamConfigReader', () => { }); }); + it('projects bootstrap-confirmed provisioned-but-not-alive launch state as settled', async () => { + const teamName = 'signal-ops'; + const teamDir = path.join(tempDir, teamName); + await fs.mkdir(teamDir, { recursive: true }); + await fs.writeFile( + path.join(teamDir, 'config.json'), + JSON.stringify({ + name: 'Signal Ops', + members: [{ name: 'team-lead', agentType: 'team-lead' }], + }), + 'utf8' + ); + await fs.writeFile( + path.join(teamDir, 'launch-state.json'), + JSON.stringify({ + version: 2, + teamName, + updatedAt: '2026-05-25T20:14:02.147Z', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + lastEvaluatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + summary: { + confirmedCount: 0, + pendingCount: 0, + failedCount: 1, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'partial_failure', + }), + 'utf8' + ); + + const reader = new TeamConfigReader(); + const teams = await reader.listTeams(); + + expect(teams).toHaveLength(1); + expect(teams[0]).toMatchObject({ + teamName, + displayName: 'Signal Ops', + teamLaunchState: 'clean_success', + confirmedMemberCount: 1, + confirmedCount: 1, + failedCount: 0, + }); + expect(teams[0]).not.toMatchObject({ + partialLaunchFailure: true, + missingMembers: ['tom'], + }); + }); + it('does not invent a partial-failure summary from artifact counts for mixed-aware teams when canonical launch truth is unavailable', async () => { const teamName = 'mixed-aware-team'; const teamDir = path.join(tempDir, teamName); @@ -578,16 +646,19 @@ describe('TeamConfigReader', () => { 'utf8' ); let ctimeMs = 1000; - vi.spyOn(nodeFs.promises, 'stat').mockImplementation(async () => ({ - size: BigInt(4096), - mode: BigInt(33188), - dev: BigInt(1), - ino: BigInt(2), - mtimeMs: 1000, - ctimeMs, - birthtimeMs: 1000, - isFile: () => true, - }) as never); + vi.spyOn(nodeFs.promises, 'stat').mockImplementation( + async () => + ({ + size: BigInt(4096), + mode: BigInt(33188), + dev: BigInt(1), + ino: BigInt(2), + mtimeMs: 1000, + ctimeMs, + birthtimeMs: 1000, + isFile: () => true, + }) as never + ); const readFileSpy = vi.spyOn(nodeFs.promises, 'readFile'); const reader = new TeamConfigReader(); @@ -682,15 +753,16 @@ describe('TeamConfigReader', () => { const readDeferred = createDeferred(); const realReadFile = nodeFs.promises.readFile.bind(nodeFs.promises); let intercepted = false; - vi.spyOn(nodeFs.promises, 'readFile').mockImplementation( - ((file: unknown, ...args: unknown[]) => { - if (!intercepted && String(file) === configPath) { - intercepted = true; - return readDeferred.promise as never; - } - return realReadFile(file as never, ...(args as never[])) as never; - }) as never - ); + vi.spyOn(nodeFs.promises, 'readFile').mockImplementation((( + file: unknown, + ...args: unknown[] + ) => { + if (!intercepted && String(file) === configPath) { + intercepted = true; + return readDeferred.promise as never; + } + return realReadFile(file as never, ...(args as never[])) as never; + }) as never); const reader = new TeamConfigReader(); const staleSnapshot = reader.getConfigSnapshot(teamName); @@ -730,15 +802,16 @@ describe('TeamConfigReader', () => { const readDeferred = createDeferred(); const realReadFile = nodeFs.promises.readFile.bind(nodeFs.promises); let intercepted = false; - vi.spyOn(nodeFs.promises, 'readFile').mockImplementation( - ((file: unknown, ...args: unknown[]) => { - if (!intercepted && String(file) === configPath) { - intercepted = true; - return readDeferred.promise as never; - } - return realReadFile(file as never, ...(args as never[])) as never; - }) as never - ); + vi.spyOn(nodeFs.promises, 'readFile').mockImplementation((( + file: unknown, + ...args: unknown[] + ) => { + if (!intercepted && String(file) === configPath) { + intercepted = true; + return readDeferred.promise as never; + } + return realReadFile(file as never, ...(args as never[])) as never; + }) as never); const reader = new TeamConfigReader(); const staleVerified = reader.getConfig(teamName); @@ -781,15 +854,16 @@ describe('TeamConfigReader', () => { const readDeferred = createDeferred(); const realReadFile = nodeFs.promises.readFile.bind(nodeFs.promises); let intercepted = false; - vi.spyOn(nodeFs.promises, 'readFile').mockImplementation( - ((file: unknown, ...args: unknown[]) => { - if (!intercepted && String(file) === configPath) { - intercepted = true; - return readDeferred.promise as never; - } - return realReadFile(file as never, ...(args as never[])) as never; - }) as never - ); + vi.spyOn(nodeFs.promises, 'readFile').mockImplementation((( + file: unknown, + ...args: unknown[] + ) => { + if (!intercepted && String(file) === configPath) { + intercepted = true; + return readDeferred.promise as never; + } + return realReadFile(file as never, ...(args as never[])) as never; + }) as never); const reader = new TeamConfigReader(); const staleSnapshot = reader.getConfigSnapshot(teamName); diff --git a/test/main/services/team/TeamLaunchSummaryProjection.test.ts b/test/main/services/team/TeamLaunchSummaryProjection.test.ts index b9319eb5..d84fbbaf 100644 --- a/test/main/services/team/TeamLaunchSummaryProjection.test.ts +++ b/test/main/services/team/TeamLaunchSummaryProjection.test.ts @@ -235,6 +235,428 @@ describe('TeamLaunchSummaryProjection', () => { }); }); + it('projects provisioned-but-not-alive failures with bootstrap proof as confirmed', () => { + const summary = choosePreferredLaunchStateSummary({ + launchSnapshot: { + version: 2, + teamName: 'signal-ops', + updatedAt: '2026-05-25T20:14:02.147Z', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + lastEvaluatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + summary: { + confirmedCount: 0, + pendingCount: 0, + failedCount: 1, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'partial_failure', + } as never, + }); + + expect(summary).toMatchObject({ + teamLaunchState: 'clean_success', + confirmedMemberCount: 1, + confirmedCount: 1, + pendingCount: 0, + failedCount: 0, + }); + expect(summary).not.toMatchObject({ + partialLaunchFailure: true, + missingMembers: ['tom'], + }); + }); + + it('projects Windows process-table-unavailable provisioned-but-not-alive metadata as confirmed', () => { + const summary = choosePreferredLaunchStateSummary({ + launchSnapshot: { + version: 2, + teamName: 'signal-ops', + updatedAt: '2026-05-25T20:14:02.147Z', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + livenessKind: 'registered_only', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + lastEvaluatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + summary: { + confirmedCount: 0, + pendingCount: 0, + failedCount: 1, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'partial_failure', + } as never, + }); + + expect(summary).toMatchObject({ + teamLaunchState: 'clean_success', + confirmedMemberCount: 1, + confirmedCount: 1, + failedCount: 0, + }); + }); + + it('keeps provisioned-but-not-alive failures with runtime error evidence as failed', () => { + const summary = choosePreferredLaunchStateSummary({ + launchSnapshot: { + version: 2, + teamName: 'signal-ops', + updatedAt: '2026-05-25T20:14:02.147Z', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + lastEvaluatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + summary: { + confirmedCount: 0, + pendingCount: 0, + failedCount: 1, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'partial_failure', + } as never, + }); + + expect(summary).toMatchObject({ + partialLaunchFailure: true, + missingMembers: ['tom'], + teamLaunchState: 'partial_failure', + confirmedCount: 0, + failedCount: 1, + }); + }); + + it('reconciles unhealed launch-summary projections with bootstrap proof', () => { + const summary = choosePreferredLaunchStateSummary({ + bootstrapSnapshot: { + version: 2, + teamName: 'signal-ops', + updatedAt: '2026-05-25T20:13:56.110Z', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + launchState: 'confirmed_alive', + agentToolAccepted: true, + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + livenessKind: 'confirmed_bootstrap', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + lastEvaluatedAt: '2026-05-25T20:13:56.110Z', + }, + }, + summary: { + confirmedCount: 1, + pendingCount: 0, + failedCount: 0, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'clean_success', + } as never, + launchSummaryProjection: { + version: 1, + teamName: 'signal-ops', + updatedAt: '2026-05-25T20:14:02.147Z', + launchUpdatedAt: '2026-05-25T20:14:02.147Z', + teamLaunchState: 'partial_failure', + partialLaunchFailure: true, + expectedMemberCount: 1, + confirmedMemberCount: 0, + missingMembers: ['tom'], + confirmedCount: 0, + pendingCount: 0, + failedCount: 1, + runtimeAlivePendingCount: 0, + }, + }); + + expect(summary).toMatchObject({ + teamLaunchState: 'clean_success', + confirmedMemberCount: 1, + confirmedCount: 1, + failedCount: 0, + pendingCount: 0, + }); + expect(summary).not.toMatchObject({ + partialLaunchFailure: true, + missingMembers: ['tom'], + }); + }); + + it('does not reconcile launch-summary projections from stale bootstrap proof', () => { + const summary = choosePreferredLaunchStateSummary({ + bootstrapSnapshot: { + version: 2, + teamName: 'signal-ops', + updatedAt: '2026-05-25T20:10:10.000Z', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + launchState: 'confirmed_alive', + agentToolAccepted: true, + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + livenessKind: 'confirmed_bootstrap', + firstSpawnAcceptedAt: '2026-05-25T20:10:00.000Z', + lastHeartbeatAt: '2026-05-25T20:10:05.000Z', + lastEvaluatedAt: '2026-05-25T20:10:10.000Z', + }, + }, + summary: { + confirmedCount: 1, + pendingCount: 0, + failedCount: 0, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'clean_success', + } as never, + launchSummaryProjection: { + version: 1, + teamName: 'signal-ops', + updatedAt: '2026-05-25T20:14:02.147Z', + launchUpdatedAt: '2026-05-25T20:14:02.147Z', + teamLaunchState: 'partial_failure', + partialLaunchFailure: true, + expectedMemberCount: 1, + confirmedMemberCount: 0, + missingMembers: ['tom'], + confirmedCount: 0, + pendingCount: 0, + failedCount: 1, + runtimeAlivePendingCount: 0, + }, + }); + + expect(summary).toMatchObject({ + partialLaunchFailure: true, + missingMembers: ['tom'], + teamLaunchState: 'partial_failure', + confirmedCount: 0, + failedCount: 1, + }); + }); + + it('does not reconcile launch-summary projections from stopped bootstrap proof', () => { + const summary = choosePreferredLaunchStateSummary({ + bootstrapSnapshot: { + version: 2, + teamName: 'signal-ops', + updatedAt: '2026-05-25T20:13:56.110Z', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + launchState: 'confirmed_alive', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: false, + livenessKind: 'not_found', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastEvaluatedAt: '2026-05-25T20:13:56.110Z', + }, + }, + summary: { + confirmedCount: 1, + pendingCount: 0, + failedCount: 0, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'clean_success', + } as never, + launchSummaryProjection: { + version: 1, + teamName: 'signal-ops', + updatedAt: '2026-05-25T20:14:02.147Z', + launchUpdatedAt: '2026-05-25T20:14:02.147Z', + teamLaunchState: 'partial_failure', + partialLaunchFailure: true, + expectedMemberCount: 1, + confirmedMemberCount: 0, + missingMembers: ['tom'], + confirmedCount: 0, + pendingCount: 0, + failedCount: 1, + runtimeAlivePendingCount: 0, + }, + }); + + expect(summary).toMatchObject({ + partialLaunchFailure: true, + missingMembers: ['tom'], + teamLaunchState: 'partial_failure', + confirmedCount: 0, + failedCount: 1, + }); + }); + + it('keeps provisioned-but-not-alive failures without bootstrap proof as failed', () => { + const summary = choosePreferredLaunchStateSummary({ + launchSnapshot: { + version: 2, + teamName: 'signal-ops', + updatedAt: '2026-05-25T20:14:02.147Z', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: false, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive', + lastEvaluatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + summary: { + confirmedCount: 0, + pendingCount: 0, + failedCount: 1, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'partial_failure', + } as never, + }); + + expect(summary).toMatchObject({ + partialLaunchFailure: true, + missingMembers: ['tom'], + teamLaunchState: 'partial_failure', + confirmedCount: 0, + failedCount: 1, + }); + }); + + it('does not project provisioned-but-not-alive from stale bootstrap proof before spawn acceptance', () => { + const summary = choosePreferredLaunchStateSummary({ + bootstrapSnapshot: { + version: 2, + teamName: 'signal-ops', + updatedAt: '2026-05-25T20:10:10.000Z', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + launchState: 'confirmed_alive', + agentToolAccepted: true, + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + firstSpawnAcceptedAt: '2026-05-25T20:10:00.000Z', + lastHeartbeatAt: '2026-05-25T20:10:05.000Z', + lastEvaluatedAt: '2026-05-25T20:10:10.000Z', + }, + }, + summary: { + confirmedCount: 1, + pendingCount: 0, + failedCount: 0, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'clean_success', + } as never, + launchSnapshot: { + version: 2, + teamName: 'signal-ops', + updatedAt: '2026-05-25T20:14:02.147Z', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: false, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastEvaluatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + summary: { + confirmedCount: 0, + pendingCount: 0, + failedCount: 1, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'partial_failure', + } as never, + }); + + expect(summary).toMatchObject({ + partialLaunchFailure: true, + missingMembers: ['tom'], + teamLaunchState: 'partial_failure', + confirmedCount: 0, + failedCount: 1, + }); + }); + it('prefers a mixed-aware persisted summary projection over a newer but poorer bootstrap snapshot', () => { const bootstrapSnapshot = { version: 2, diff --git a/test/main/services/team/TeamProvisioningLaunchDiagnostics.test.ts b/test/main/services/team/TeamProvisioningLaunchDiagnostics.test.ts index d05f9957..bfb19a79 100644 --- a/test/main/services/team/TeamProvisioningLaunchDiagnostics.test.ts +++ b/test/main/services/team/TeamProvisioningLaunchDiagnostics.test.ts @@ -21,7 +21,7 @@ function spawnEntry(overrides: Partial): MemberSpawnStat }; } -function buildRun(entries: Array<[string, Partial]>, isLaunch = true) { +function buildRun(entries: [string, Partial][], isLaunch = true) { return { isLaunch, memberSpawnStatuses: new Map( @@ -215,6 +215,139 @@ describe('TeamProvisioningLaunchDiagnostics', () => { ]); }); + it('classifies bootstrap-confirmed provisioned-but-not-alive entries as confirmed', () => { + const diagnostics = buildLaunchDiagnosticsFromRun( + buildRun([ + [ + 'tom', + { + status: 'error', + launchState: 'failed_to_start', + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + }, + ], + ]), + { nowIso } + ); + + expect(diagnostics).toEqual([ + { + id: 'tom:bootstrap_confirmed', + memberName: 'tom', + severity: 'info', + code: 'bootstrap_confirmed', + label: 'tom - bootstrap confirmed', + observedAt: NOW, + }, + ]); + }); + + it('classifies process-table-unavailable registered metadata as confirmed', () => { + const diagnostics = buildLaunchDiagnosticsFromRun( + buildRun([ + [ + 'tom', + { + status: 'error', + launchState: 'failed_to_start', + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + livenessKind: 'registered_only', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + }, + ], + ]), + { nowIso } + ); + + expect(diagnostics).toEqual([ + { + id: 'tom:bootstrap_confirmed', + memberName: 'tom', + severity: 'info', + code: 'bootstrap_confirmed', + label: 'tom - bootstrap confirmed', + observedAt: NOW, + }, + ]); + }); + + it('keeps error diagnostics for bootstrap-confirmed provisioned-but-not-alive entries', () => { + const diagnostics = buildLaunchDiagnosticsFromRun( + buildRun([ + [ + 'tom', + { + status: 'error', + launchState: 'failed_to_start', + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }, + ], + ]), + { nowIso } + ); + + expect(diagnostics).toEqual([ + { + id: 'tom:bootstrap_stalled', + memberName: 'tom', + severity: 'error', + code: 'bootstrap_stalled', + label: 'tom - launch diagnostic error', + detail: 'Runtime process crashed', + observedAt: NOW, + }, + ]); + }); + + it('keeps stopped liveness diagnostics for bootstrap-confirmed provisioned-but-not-alive entries', () => { + const diagnostics = buildLaunchDiagnosticsFromRun( + buildRun([ + [ + 'tom', + { + status: 'error', + launchState: 'failed_to_start', + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + }, + ], + ]), + { nowIso } + ); + + expect(diagnostics).toEqual([ + { + id: 'tom:bootstrap_stalled', + memberName: 'tom', + severity: 'error', + code: 'bootstrap_stalled', + label: 'tom - launch diagnostic error', + detail: 'Runtime is no longer registered', + observedAt: NOW, + }, + ]); + }); + it('uses failed launch error when hard failure reason is absent', () => { expect( buildLaunchDiagnosticsFromRun( diff --git a/test/main/services/team/TeamProvisioningLaunchFailurePolicy.test.ts b/test/main/services/team/TeamProvisioningLaunchFailurePolicy.test.ts index 9d0d6f38..99a94de7 100644 --- a/test/main/services/team/TeamProvisioningLaunchFailurePolicy.test.ts +++ b/test/main/services/team/TeamProvisioningLaunchFailurePolicy.test.ts @@ -3,6 +3,7 @@ import { isAutoClearableLaunchFailureReason, isBootstrapCheckInTimeoutFailureReason, isBootstrapInstructionPromptFailureReason, + isCliProvisionedButNotAliveFailureReason, isBootstrapMcpResourceReadFailureReason, isConfigRegistrationFailureReason, isLaunchCleanupBootstrapIncompleteFailureReason, @@ -10,9 +11,11 @@ import { isNeverSpawnedDuringLaunchReason, isOpenCodeBridgeLaunchFailureReason, isProcessTableUnavailableFailureReason, + isProvisionedButNotAliveFailureReason, isRegisteredRuntimeMetadataFailureReason, stripProcessTableUnavailableDiagnosticSuffix, } from '@main/services/team/provisioning/TeamProvisioningLaunchFailurePolicy'; +import { isBootstrapConfirmedProvisionedButNotAliveFailure } from '@shared/utils/teamLaunchFailureReason'; import { describe, expect, it } from 'vitest'; describe('TeamProvisioningLaunchFailurePolicy', () => { @@ -28,12 +31,27 @@ describe('TeamProvisioningLaunchFailurePolicy', () => { 'Teammate was not registered in config.json during launch. Persistent spawn failed.' ) ).toBe(true); - expect(isOpenCodeBridgeLaunchFailureReason('OpenCode bridge reported member launch failure')).toBe( - true - ); + expect( + isOpenCodeBridgeLaunchFailureReason('OpenCode bridge reported member launch failure') + ).toBe(true); expect( isRegisteredRuntimeMetadataFailureReason('registered runtime metadata without live process') ).toBe(true); + expect( + isProvisionedButNotAliveFailureReason( + 'CLI process exited (code 1) \u2014 team provisioned but not alive' + ) + ).toBe(true); + expect( + isProvisionedButNotAliveFailureReason( + 'CLI process exited (code unknown) - team provisioned but not alive; process table unavailable' + ) + ).toBe(true); + expect( + isCliProvisionedButNotAliveFailureReason( + 'CLI process exited (code ?) - team provisioned but not alive' + ) + ).toBe(true); }); it('recognizes bootstrap-specific failure reasons without accepting unrelated text', () => { @@ -42,9 +60,9 @@ describe('TeamProvisioningLaunchFailurePolicy', () => { 'resources/read failed for member_briefing: MCP error method not found' ) ).toBe(true); - expect(isBootstrapMcpResourceReadFailureReason('resources/read failed for other resource')).toBe( - false - ); + expect( + isBootstrapMcpResourceReadFailureReason('resources/read failed for other resource') + ).toBe(false); expect( isBootstrapCheckInTimeoutFailureReason( 'Teammate was registered but did not bootstrap-confirm before timeout.' @@ -69,9 +87,9 @@ describe('TeamProvisioningLaunchFailurePolicy', () => { 'runtime pid could not be verified because process table is unavailable' ) ).toBe(true); - expect(isProcessTableUnavailableFailureReason('runtime failed; process table unavailable')).toBe( - false - ); + expect( + isProcessTableUnavailableFailureReason('runtime failed; process table unavailable') + ).toBe(false); expect( stripProcessTableUnavailableDiagnosticSuffix( 'Teammate did not join within the launch grace window.; process table unavailable' @@ -80,9 +98,9 @@ describe('TeamProvisioningLaunchFailurePolicy', () => { }); it('keeps auto-clear policy narrow but accepts known recoverable suffixes', () => { - expect( - isAutoClearableLaunchFailureReason('Teammate was never spawned during launch.') - ).toBe(true); + expect(isAutoClearableLaunchFailureReason('Teammate was never spawned during launch.')).toBe( + true + ); expect(isAutoClearableLaunchFailureReason('process table is unavailable')).toBe(true); expect( isAutoClearableLaunchFailureReason( @@ -91,11 +109,57 @@ describe('TeamProvisioningLaunchFailurePolicy', () => { ).toBe(true); expect( isAutoClearableLaunchFailureReason( - 'CLI process exited (code 1) — team provisioned but not alive' + 'CLI process exited (code 1) \u2014 team provisioned but not alive' ) - ).toBe(true); + ).toBe(false); expect(isAutoClearableLaunchFailureReason('model not found')).toBe(false); - expect(isAutoClearableLaunchFailureReason(undefined)).toBe(false); + expect(isAutoClearableLaunchFailureReason()).toBe(false); + }); + + it('requires bootstrap proof before treating provisioned-but-not-alive as healed', () => { + const reason = 'CLI process exited (code 1) \u2014 team provisioned but not alive'; + + expect( + isBootstrapConfirmedProvisionedButNotAliveFailure({ + status: 'error', + launchState: 'failed_to_start', + hardFailure: true, + error: reason, + bootstrapConfirmed: true, + }) + ).toBe(true); + + expect( + isBootstrapConfirmedProvisionedButNotAliveFailure({ + status: 'error', + launchState: 'failed_to_start', + hardFailure: true, + hardFailureReason: 'model not found', + error: reason, + bootstrapConfirmed: true, + }) + ).toBe(false); + + expect( + isBootstrapConfirmedProvisionedButNotAliveFailure({ + status: 'error', + launchState: 'failed_to_start', + hardFailure: true, + hardFailureReason: reason, + bootstrapConfirmed: false, + livenessKind: 'registered_only', + }) + ).toBe(false); + + expect( + isBootstrapConfirmedProvisionedButNotAliveFailure({ + status: 'error', + launchState: 'failed_to_start', + hardFailure: true, + runtimeDiagnostic: reason, + bootstrapConfirmed: true, + }) + ).toBe(true); }); it('derives member launch state by the existing precedence order', () => { @@ -110,9 +174,7 @@ describe('TeamProvisioningLaunchFailurePolicy', () => { 'runtime_pending_permission' ); expect(deriveMemberLaunchState({ runtimeAlive: true })).toBe('runtime_pending_bootstrap'); - expect(deriveMemberLaunchState({ agentToolAccepted: true })).toBe( - 'runtime_pending_bootstrap' - ); + expect(deriveMemberLaunchState({ agentToolAccepted: true })).toBe('runtime_pending_bootstrap'); expect(deriveMemberLaunchState({})).toBe('starting'); }); }); diff --git a/test/main/services/team/TeamProvisioningPromptBuilders.test.ts b/test/main/services/team/TeamProvisioningPromptBuilders.test.ts new file mode 100644 index 00000000..f1d5ef9a --- /dev/null +++ b/test/main/services/team/TeamProvisioningPromptBuilders.test.ts @@ -0,0 +1,59 @@ +import { buildGeminiPostLaunchHydrationPrompt } from '@main/services/team/provisioning/TeamProvisioningPromptBuilders'; +import { describe, expect, it } from 'vitest'; + +import type { MemberSpawnStatusEntry, TeamCreateRequest } from '@shared/types'; + +function buildPromptWithStatus(status: MemberSpawnStatusEntry): string { + return buildGeminiPostLaunchHydrationPrompt( + { + teamName: 'signal-ops', + request: { prompt: 'Check readiness.' }, + memberSpawnStatuses: new Map([['tom', status]]), + }, + 'lead', + [{ name: 'tom', providerId: 'anthropic', model: 'sonnet' }] as TeamCreateRequest['members'], + [] + ); +} + +describe('TeamProvisioningPromptBuilders', () => { + it('keeps errored provisioned-but-not-alive members failed in Gemini hydration prompts', () => { + const prompt = buildPromptWithStatus({ + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + updatedAt: '2026-05-25T20:14:02.147Z', + }); + + expect(prompt).toContain( + '- @tom: failed to start - CLI process exited (code 1) - team provisioned but not alive' + ); + expect(prompt).not.toContain('- @tom: bootstrap confirmed'); + }); + + it('keeps benign provisioned-but-not-alive members confirmed in Gemini hydration prompts', () => { + const prompt = buildPromptWithStatus({ + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }); + + expect(prompt).toContain('- @tom: bootstrap confirmed'); + expect(prompt).not.toContain('- @tom: failed to start'); + }); +}); diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index 6e8c9204..c9f6fc91 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -495,8 +495,7 @@ async function writeCommittedOpenCodeSessionStore(input: { { clock: () => new Date('2026-04-22T12:00:00.000Z'), batchIdFactory: () => `batch-${input.runId}${input.batchKey ? `-${input.batchKey}` : ''}`, - receiptIdFactory: () => - `receipt-${input.runId}${input.batchKey ? `-${input.batchKey}` : ''}`, + receiptIdFactory: () => `receipt-${input.runId}${input.batchKey ? `-${input.batchKey}` : ''}`, } ); await writer.writeBatch({ @@ -618,11 +617,12 @@ type TeamProvisioningServicePrivateHarness = { applyBootstrapTranscriptEvidenceOverlay: ( snapshot: ReturnType | null ) => Promise | null>; + applyProcessBootstrapTransportOverlay: ( + input: Record + ) => Record; }; -function privateHarness( - svc: TeamProvisioningService -): TeamProvisioningServicePrivateHarness { +function privateHarness(svc: TeamProvisioningService): TeamProvisioningServicePrivateHarness { return svc as unknown as TeamProvisioningServicePrivateHarness; } @@ -858,20 +858,12 @@ describe('TeamProvisioningService', () => { lastLeadTextEmitMs: 0, }; - internals.pushLiveLeadTextMessage( - run, - 'Соз', - undefined, - '2026-04-17T12:00:00.000Z', - { coalesceStreamChunk: true } - ); - internals.pushLiveLeadTextMessage( - run, - 'дал', - undefined, - '2026-04-17T12:00:00.010Z', - { coalesceStreamChunk: true } - ); + internals.pushLiveLeadTextMessage(run, 'Соз', undefined, '2026-04-17T12:00:00.000Z', { + coalesceStreamChunk: true, + }); + internals.pushLiveLeadTextMessage(run, 'дал', undefined, '2026-04-17T12:00:00.010Z', { + coalesceStreamChunk: true, + }); internals.pushLiveLeadTextMessage( run, ' стартовую задачу', @@ -2641,17 +2633,9 @@ describe('TeamProvisioningService', () => { internals.setLeadActivity(run, 'idle'); expect(resumeSpy).toHaveBeenCalledTimes(1); - expect(resumeSpy).toHaveBeenCalledWith( - teamName, - 'team-lead', - '2026-05-02T10:05:00.000Z' - ); + expect(resumeSpy).toHaveBeenCalledWith(teamName, 'team-lead', '2026-05-02T10:05:00.000Z'); expect(pauseSpy).toHaveBeenCalledTimes(1); - expect(pauseSpy).toHaveBeenCalledWith( - teamName, - 'team-lead', - '2026-05-02T10:05:00.000Z' - ); + expect(pauseSpy).toHaveBeenCalledWith(teamName, 'team-lead', '2026-05-02T10:05:00.000Z'); const staleRun = toLeadActivityTestRun( { @@ -8647,17 +8631,15 @@ describe('TeamProvisioningService', () => { generation: 2, observedAt: '2026-04-25T10:00:00.000Z', }; - const transportSpy = vi - .spyOn(agentTeamsMcpHttpServer, 'getCurrentHandle') - .mockReturnValue({ - url: currentTransportEvidence.url, - port: currentTransportEvidence.port, - child: { pid: 43123 }, - generation: currentTransportEvidence.generation, - urlHash: currentTransportEvidence.urlHash, - transportEvidence: currentTransportEvidence, - diagnostics: [], - } as any); + const transportSpy = vi.spyOn(agentTeamsMcpHttpServer, 'getCurrentHandle').mockReturnValue({ + url: currentTransportEvidence.url, + port: currentTransportEvidence.port, + child: { pid: 43123 }, + generation: currentTransportEvidence.generation, + urlHash: currentTransportEvidence.urlHash, + transportEvidence: currentTransportEvidence, + diagnostics: [], + } as any); try { await expect( @@ -8730,59 +8712,56 @@ describe('TeamProvisioningService', () => { generation: 3, observedAt: '2026-04-25T10:00:00.000Z', }; - const transportSpy = vi - .spyOn(agentTeamsMcpHttpServer, 'getCurrentHandle') - .mockReturnValue({ - url: currentTransportEvidence.url, - port: currentTransportEvidence.port, - child: { pid: 43124 }, - generation: currentTransportEvidence.generation, - urlHash: currentTransportEvidence.urlHash, - transportEvidence: currentTransportEvidence, - diagnostics: [], - } as any); + const transportSpy = vi.spyOn(agentTeamsMcpHttpServer, 'getCurrentHandle').mockReturnValue({ + url: currentTransportEvidence.url, + port: currentTransportEvidence.port, + child: { pid: 43124 }, + generation: currentTransportEvidence.generation, + urlHash: currentTransportEvidence.urlHash, + transportEvidence: currentTransportEvidence, + diagnostics: [], + } as any); const directBridgeExecute = vi.fn(async () => { throw new Error('direct OpenCode bridge executor should not be used for acceptance send'); }); - const stateChangingExecute = vi.fn(async (input: { - command: string; - body: Record; - }) => ({ - ok: true as const, - schemaVersion: OPEN_CODE_BRIDGE_SCHEMA_VERSION, - requestId: 'send-refresh-command', - command: input.command as any, - completedAt: '2026-04-25T10:00:01.000Z', - durationMs: 10, - runtime: { - providerId: 'opencode' as const, - binaryPath: '/opt/homebrew/bin/opencode', - binaryFingerprint: 'test-opencode-binary', - version: '1.0.0', - capabilitySnapshotId: 'test-capability-snapshot', - }, - diagnostics: [], - data: { - accepted: true, - memberName: 'bob', - sessionId: 'oc-session-bob-production-refresh', - runtimePid: 456, - runtimePromptMessageId: 'msg_prompt_production_refresh', - prePromptCursor: 'cursor-production-refresh', - responseObservation: { - state: 'pending', - deliveredUserMessageId: 'oc-user-production-refresh', - assistantMessageId: null, - toolCallNames: [], - visibleMessageToolCallId: null, - visibleReplyMessageId: null, - visibleReplyCorrelation: null, - latestAssistantPreview: null, - reason: 'assistant_response_pending', + const stateChangingExecute = vi.fn( + async (input: { command: string; body: Record }) => ({ + ok: true as const, + schemaVersion: OPEN_CODE_BRIDGE_SCHEMA_VERSION, + requestId: 'send-refresh-command', + command: input.command as any, + completedAt: '2026-04-25T10:00:01.000Z', + durationMs: 10, + runtime: { + providerId: 'opencode' as const, + binaryPath: '/opt/homebrew/bin/opencode', + binaryFingerprint: 'test-opencode-binary', + version: '1.0.0', + capabilitySnapshotId: 'test-capability-snapshot', }, diagnostics: [], - }, - })); + data: { + accepted: true, + memberName: 'bob', + sessionId: 'oc-session-bob-production-refresh', + runtimePid: 456, + runtimePromptMessageId: 'msg_prompt_production_refresh', + prePromptCursor: 'cursor-production-refresh', + responseObservation: { + state: 'pending', + deliveredUserMessageId: 'oc-user-production-refresh', + assistantMessageId: null, + toolCallNames: [], + visibleMessageToolCallId: null, + visibleReplyMessageId: null, + visibleReplyCorrelation: null, + latestAssistantPreview: null, + reason: 'assistant_response_pending', + }, + diagnostics: [], + }, + }) + ); const productionBridge = new OpenCodeReadinessBridge( { execute: directBridgeExecute }, { stateChangingCommands: { execute: stateChangingExecute as any } } @@ -8904,17 +8883,15 @@ describe('TeamProvisioningService', () => { generation: 7, observedAt: '2026-04-25T10:00:00.000Z', }; - const transportSpy = vi - .spyOn(agentTeamsMcpHttpServer, 'getCurrentHandle') - .mockReturnValue({ - url: currentTransportEvidence.url, - port: currentTransportEvidence.port, - child: { pid: 43128 }, - generation: currentTransportEvidence.generation, - urlHash: currentTransportEvidence.urlHash, - transportEvidence: currentTransportEvidence, - diagnostics: [], - } as any); + const transportSpy = vi.spyOn(agentTeamsMcpHttpServer, 'getCurrentHandle').mockReturnValue({ + url: currentTransportEvidence.url, + port: currentTransportEvidence.port, + child: { pid: 43128 }, + generation: currentTransportEvidence.generation, + urlHash: currentTransportEvidence.urlHash, + transportEvidence: currentTransportEvidence, + diagnostics: [], + } as any); try { await expect( @@ -9264,7 +9241,9 @@ describe('TeamProvisioningService', () => { { label: 'resolved behavior changes', staleReason: 'resolved_behavior_changed:old->new', - staleDiagnostics: ['OpenCode session reconcile skipped because the stored session is stale'], + staleDiagnostics: [ + 'OpenCode session reconcile skipped because the stored session is stale', + ], }, { label: 'action-required reasons', @@ -16618,9 +16597,7 @@ describe('TeamProvisioningService', () => { ] as never); mcpConfigBuilder.writeConfigFile.mockImplementation(async (_projectPath, policy) => { const mode = getMockMcpPolicyMode(policy); - return mode === 'appOnly' - ? '/mock/member-mcp-app-only.json' - : '/mock/lead-mcp-config.json'; + return mode === 'appOnly' ? '/mock/member-mcp-app-only.json' : '/mock/lead-mcp-config.json'; }); const { runId } = await svc.launchTeam( @@ -19465,9 +19442,7 @@ describe('TeamProvisioningService', () => { }; expect(persisted.teamLaunchState).toBe('partial_failure'); expect(persisted.members?.alice?.launchState).toBe('failed_to_start'); - expect(persisted.members?.alice?.hardFailureReason).toContain( - 'team provisioned but not alive' - ); + expect(persisted.members?.alice?.hardFailureReason).toContain('team provisioned but not alive'); const reconciled = await (svc as any).reconcilePersistedLaunchState(teamName); expect(reconciled.snapshot?.teamLaunchState).toBe('partial_failure'); @@ -20429,6 +20404,188 @@ describe('TeamProvisioningService', () => { expect(result.statuses.jack?.runtimeDiagnosticSeverity).toBeUndefined(); }); + it('heals provisioned-but-not-alive launch failures when bootstrap-state confirms the member', async () => { + allowConsoleLogs(); + const teamName = 'zz-unit-provisioned-not-alive-bootstrap-state-heals'; + const leadSessionId = 'lead-session'; + const projectPath = '/Users/test/proj'; + const acceptedAt = new Date(Date.now() - 90_000).toISOString(); + const bootstrapAt = new Date(Date.now() - 60_000).toISOString(); + const cleanupAt = new Date(Date.now() - 30_000).toISOString(); + const runtimePid = 27_036; + const exitReason = 'CLI process exited (code 1) \u2014 team provisioned but not alive'; + const processTableReason = + 'runtime pid could not be verified because process table is unavailable'; + + writeLaunchConfig(teamName, projectPath, leadSessionId, ['tom']); + writeLaunchState( + teamName, + leadSessionId, + { + tom: { + providerId: 'anthropic', + model: 'sonnet', + laneId: 'primary', + laneKind: 'primary', + laneOwnerProviderId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + runtimePid, + bootstrapConfirmed: false, + hardFailure: true, + hardFailureReason: exitReason, + livenessKind: 'registered_only', + runtimeDiagnostic: processTableReason, + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: acceptedAt, + lastEvaluatedAt: cleanupAt, + }, + }, + { launchPhase: 'finished', updatedAt: cleanupAt } + ); + writeBootstrapState( + teamName, + [ + { + name: 'tom', + status: 'bootstrap_confirmed', + lastAttemptAt: Date.parse(acceptedAt), + lastObservedAt: Date.parse(bootstrapAt), + }, + ], + bootstrapAt + ); + + const svc = new TeamProvisioningService(); + privateHarness(svc).getLiveTeamAgentRuntimeMetadata = vi.fn( + async () => + new Map([ + [ + 'tom', + { + alive: false, + backendType: 'process', + providerId: 'anthropic', + livenessKind: 'registered_only', + pidSource: 'persisted_metadata', + runtimeDiagnostic: processTableReason, + runtimeDiagnosticSeverity: 'warning', + metricsPid: runtimePid, + model: 'sonnet', + }, + ], + ]) + ); + + const result = await svc.getMemberSpawnStatuses(teamName); + + expect(result.teamLaunchState).toBe('clean_success'); + expect(result.statuses.tom).toMatchObject({ + status: 'online', + launchState: 'confirmed_alive', + bootstrapConfirmed: true, + runtimeAlive: true, + livenessKind: 'confirmed_bootstrap', + hardFailure: false, + error: undefined, + }); + expect(result.statuses.tom?.hardFailureReason).toBeUndefined(); + expect(result.statuses.tom?.runtimeDiagnostic).toBeUndefined(); + expect(result.statuses.tom?.runtimeDiagnosticSeverity).toBeUndefined(); + }); + + it('does not heal provisioned-but-not-alive live status when refreshed runtime metadata is unsafe', async () => { + allowConsoleLogs(); + const teamName = 'zz-unit-provisioned-not-alive-live-runtime-error-stays-failed'; + const leadSessionId = 'lead-session'; + const projectPath = '/Users/test/proj'; + const acceptedAt = new Date(Date.now() - 90_000).toISOString(); + const bootstrapAt = new Date(Date.now() - 60_000).toISOString(); + const cleanupAt = new Date(Date.now() - 30_000).toISOString(); + const runtimePid = 27_036; + const exitReason = + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable'; + const processTableReason = + 'runtime pid could not be verified because process table is unavailable'; + + writeLaunchConfig(teamName, projectPath, leadSessionId, ['tom']); + writeLaunchState( + teamName, + leadSessionId, + { + tom: { + providerId: 'anthropic', + model: 'sonnet', + laneId: 'primary', + laneKind: 'primary', + laneOwnerProviderId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + runtimePid, + bootstrapConfirmed: false, + hardFailure: true, + hardFailureReason: exitReason, + livenessKind: 'registered_only', + runtimeDiagnostic: processTableReason, + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: acceptedAt, + lastEvaluatedAt: cleanupAt, + }, + }, + { launchPhase: 'finished', updatedAt: cleanupAt } + ); + writeBootstrapState( + teamName, + [ + { + name: 'tom', + status: 'bootstrap_confirmed', + lastAttemptAt: Date.parse(acceptedAt), + lastObservedAt: Date.parse(bootstrapAt), + }, + ], + bootstrapAt + ); + + const svc = new TeamProvisioningService(); + privateHarness(svc).getLiveTeamAgentRuntimeMetadata = vi.fn( + async () => + new Map([ + [ + 'tom', + { + alive: false, + backendType: 'process', + providerId: 'anthropic', + livenessKind: 'not_found', + pidSource: 'process_table', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + metricsPid: runtimePid, + model: 'sonnet', + }, + ], + ]) + ); + + const result = await svc.getMemberSpawnStatuses(teamName); + + expect(result.teamLaunchState).toBe('partial_failure'); + expect(result.statuses.tom).toMatchObject({ + status: 'error', + launchState: 'failed_to_start', + bootstrapConfirmed: true, + runtimeAlive: false, + livenessKind: 'not_found', + hardFailure: true, + hardFailureReason: exitReason, + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }); + }); + it('heals process-table unavailable failure when Anthropic bootstrap confirmation slightly predates delayed app acceptance', async () => { allowConsoleLogs(); const teamName = 'zz-unit-process-table-unavailable-bootstrap-skew-heals'; @@ -21183,6 +21340,36 @@ describe('TeamProvisioningService', () => { ); }); + it('does not downgrade provisioned-but-not-alive failures from process transport progress alone', () => { + const svc = new TeamProvisioningService(); + const reason = 'CLI process exited (code 1) \u2014 team provisioned but not alive'; + const result = privateHarness(svc).applyProcessBootstrapTransportOverlay({ + member: { + name: 'jack', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: false, + hardFailure: true, + hardFailureReason: reason, + lastEvaluatedAt: '2026-05-25T20:14:05.411Z', + }, + summary: { + hasProgress: true, + submitted: true, + lastStage: 'bootstrap submitted', + }, + launchPhase: 'active', + }); + + expect(result).toMatchObject({ + launchState: 'failed_to_start', + bootstrapConfirmed: false, + hardFailure: true, + hardFailureReason: reason, + }); + }); + it('uses the last process transport stage when active launch grace expires', async () => { allowConsoleLogs(); const teamName = 'zz-unit-process-bootstrap-transport-timeout'; @@ -22426,7 +22613,7 @@ describe('TeamProvisioningService', () => { }); }); - it('marks a live teammate bootstrap as confirmed from transcript even when runtime discovery is stale', async () => { + it('marks a live teammate bootstrap as confirmed from transcript without claiming runtime is alive', async () => { allowConsoleLogs(); const teamName = 'zz-live-bootstrap-transcript-success-without-runtime'; const leadSessionId = 'lead-session'; @@ -23187,7 +23374,11 @@ describe('TeamProvisioningService', () => { fs.mkdirSync(worktreeGitDir, { recursive: true }); fs.writeFileSync(path.join(worktreeDir, '.git'), `gitdir: ${worktreeGitDir}\n`, 'utf8'); fs.writeFileSync(path.join(worktreeGitDir, 'commondir'), '../..\n', 'utf8'); - fs.writeFileSync(path.join(worktreeGitDir, 'gitdir'), `${path.join(worktreeDir, '.git')}\n`, 'utf8'); + fs.writeFileSync( + path.join(worktreeGitDir, 'gitdir'), + `${path.join(worktreeDir, '.git')}\n`, + 'utf8' + ); const workspaces = await harness.collectWorkspaceTrustWorkspaces({ cwd: repoDir, @@ -23202,9 +23393,7 @@ describe('TeamProvisioningService', () => { gitRootConfigKey: repoDir, memberId: 'alice', }); - expect(memberWorktrees.every((workspace) => workspace.gitRootConfigKey === repoDir)).toBe( - true - ); + expect(memberWorktrees.every((workspace) => workspace.gitRootConfigKey === repoDir)).toBe(true); }); it('degrades workspace trust planning failures without blocking launch preparation', async () => { @@ -23835,7 +24024,8 @@ describe('TeamProvisioningService', () => { { alive: false, livenessKind: 'registered_only', - runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', runtimeDiagnosticSeverity: 'warning', }, ], @@ -23869,6 +24059,164 @@ describe('TeamProvisioningService', () => { }); }); + it('clears provisioned-but-not-alive failure from confirmed bootstrap even with weak metadata', async () => { + const svc = new TeamProvisioningService(); + const harness = privateHarness(svc); + const processTableReason = + 'runtime pid could not be verified because process table is unavailable'; + const exitReason = 'CLI process exited (code 1) \u2014 team provisioned but not alive'; + harness.getLiveTeamAgentRuntimeMetadata = vi.fn( + async () => + new Map([ + [ + 'tom', + { + alive: false, + model: 'sonnet', + livenessKind: 'registered_only', + pidSource: 'persisted_metadata', + runtimeDiagnostic: processTableReason, + runtimeDiagnosticSeverity: 'warning', + }, + ], + ]) + ); + + const result = await harness.attachLiveRuntimeMetadataToStatuses('signal-ops', { + tom: createMemberSpawnStatusEntry({ + status: 'error', + launchState: 'failed_to_start', + error: exitReason, + hardFailure: true, + hardFailureReason: exitReason, + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: processTableReason, + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + }), + }); + + expect(result.tom).toMatchObject({ + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + hardFailureReason: undefined, + error: undefined, + runtimeModel: 'sonnet', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: undefined, + runtimeDiagnosticSeverity: undefined, + livenessSource: undefined, + }); + }); + + it('does not let weak metadata undo confirmed bootstrap failure healing', async () => { + const svc = new TeamProvisioningService(); + const harness = privateHarness(svc); + const processTableReason = + 'runtime pid could not be verified because process table is unavailable'; + const exitReason = 'CLI process exited (code 1) \u2014 team provisioned but not alive'; + harness.getLiveTeamAgentRuntimeMetadata = vi.fn( + async () => + new Map([ + [ + 'tom', + { + alive: false, + model: 'sonnet', + livenessKind: 'registered_only', + pidSource: 'persisted_metadata', + runtimeDiagnostic: processTableReason, + runtimeDiagnosticSeverity: 'warning', + }, + ], + ]) + ); + + const result = await harness.attachLiveRuntimeMetadataToStatuses('signal-ops', { + tom: createMemberSpawnStatusEntry({ + status: 'error', + launchState: 'confirmed_alive', + error: exitReason, + hardFailure: true, + hardFailureReason: exitReason, + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: false, + runtimeDiagnostic: processTableReason, + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + }), + }); + + expect(result.tom).toMatchObject({ + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + hardFailureReason: undefined, + error: undefined, + runtimeModel: 'sonnet', + }); + }); + + it('does not keep healed confirmed-bootstrap status alive when refreshed runtime metadata is an error', async () => { + const svc = new TeamProvisioningService(); + const harness = privateHarness(svc); + harness.getLiveTeamAgentRuntimeMetadata = vi.fn( + () => + Promise.resolve( + new Map([ + [ + 'tom', + { + alive: false, + model: 'sonnet', + livenessKind: 'not_found', + pidSource: 'process_table', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }, + ], + ]) + ) + ); + + const result = await harness.attachLiveRuntimeMetadataToStatuses('signal-ops', { + tom: createMemberSpawnStatusEntry({ + status: 'online', + launchState: 'confirmed_alive', + agentToolAccepted: true, + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + livenessKind: 'confirmed_bootstrap', + runtimeModel: 'sonnet', + }), + }); + + expect(result.tom).toMatchObject({ + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: false, + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + runtimeModel: 'sonnet', + livenessSource: undefined, + }); + }); + it('does not clear OpenCode bridge launch failure from process-only liveness', async () => { const svc = new TeamProvisioningService(); (svc as any).getLiveTeamAgentRuntimeMetadata = vi.fn( @@ -26824,7 +27172,7 @@ describe('TeamProvisioningService', () => { status: 'online', launchState: 'confirmed_alive', bootstrapConfirmed: true, - runtimeAlive: false, + runtimeAlive: true, livenessKind: 'confirmed_bootstrap', hardFailure: false, error: undefined, @@ -26834,6 +27182,288 @@ describe('TeamProvisioningService', () => { expect(result.statuses.tom?.runtimeDiagnosticSeverity).toBeUndefined(); }); + it('keeps primary provisioned-but-not-alive reporting failed when runtime evidence is unsafe', async () => { + const teamName = 'primary-bootstrap-cli-provisioned-not-alive-runtime-error'; + const bootstrapRunId = 'run-primary-cli-exit-runtime-error'; + const reason = 'CLI process exited (code 1) - team provisioned but not alive'; + writeTeamMeta(teamName, { + providerId: 'anthropic', + model: 'sonnet', + }); + writeMembersMeta(teamName, [{ name: 'tom', providerId: 'anthropic', model: 'sonnet' }]); + writeLaunchConfig(teamName, '/Users/test/proj', 'lead-session', ['tom']); + writeMemberBootstrapRunId(teamName, 'tom', bootstrapRunId); + writeBootstrapState( + teamName, + [ + { + name: 'tom', + status: 'bootstrap_confirmed', + lastAttemptAt: Date.parse('2026-05-25T20:13:46.326Z'), + lastObservedAt: Date.parse('2026-05-25T20:13:56.110Z'), + }, + ], + '2026-05-25T20:14:03.317Z', + { runId: bootstrapRunId } + ); + fs.writeFileSync( + getTeamLaunchStatePath(teamName), + `${JSON.stringify( + createPersistedLaunchSnapshot({ + teamName, + leadSessionId: 'lead-session', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + model: 'sonnet', + laneId: 'primary', + laneKind: 'primary', + laneOwnerProviderId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + runtimePid: 27_036, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: reason, + livenessKind: 'confirmed_bootstrap', + pidSource: 'persisted_metadata', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + runtimeLastSeenAt: '2026-05-25T20:13:46.326Z', + lastEvaluatedAt: '2026-05-25T20:14:05.411Z', + }, + }, + updatedAt: '2026-05-25T20:14:05.411Z', + }), + null, + 2 + )}\n`, + 'utf8' + ); + + const svc = new TeamProvisioningService(); + const result = await svc.getMemberSpawnStatuses(teamName); + + expect(result.statuses.tom).toMatchObject({ + status: 'error', + launchState: 'failed_to_start', + bootstrapConfirmed: true, + runtimeAlive: false, + livenessKind: 'stale_metadata', + hardFailure: true, + hardFailureReason: reason, + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }); + expect(result.teamLaunchState).toBe('partial_failure'); + }); + + it('keeps provisioned-but-not-alive failed when refreshed runtime evidence is unsafe', async () => { + const teamName = 'primary-bootstrap-cli-provisioned-not-alive-refreshed-runtime-error'; + const bootstrapRunId = 'run-primary-cli-exit-refreshed-runtime-error'; + const reason = + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable'; + writeTeamMeta(teamName, { + providerId: 'anthropic', + model: 'sonnet', + }); + writeMembersMeta(teamName, [{ name: 'tom', providerId: 'anthropic', model: 'sonnet' }]); + writeLaunchConfig(teamName, '/Users/test/proj', 'lead-session', ['tom']); + writeMemberBootstrapRunId(teamName, 'tom', bootstrapRunId); + writeBootstrapState( + teamName, + [ + { + name: 'tom', + status: 'bootstrap_confirmed', + lastAttemptAt: Date.parse('2026-05-25T20:13:46.326Z'), + lastObservedAt: Date.parse('2026-05-25T20:13:56.110Z'), + }, + ], + '2026-05-25T20:14:03.317Z', + { runId: bootstrapRunId } + ); + fs.writeFileSync( + getTeamLaunchStatePath(teamName), + `${JSON.stringify( + createPersistedLaunchSnapshot({ + teamName, + leadSessionId: 'lead-session', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + model: 'sonnet', + laneId: 'primary', + laneKind: 'primary', + laneOwnerProviderId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + runtimePid: 27_036, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: reason, + livenessKind: 'registered_only', + pidSource: 'persisted_metadata', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + runtimeLastSeenAt: '2026-05-25T20:13:46.326Z', + lastEvaluatedAt: '2026-05-25T20:14:05.411Z', + }, + }, + updatedAt: '2026-05-25T20:14:05.411Z', + }), + null, + 2 + )}\n`, + 'utf8' + ); + + const svc = new TeamProvisioningService(); + (svc as any).getLiveTeamAgentRuntimeMetadata = vi.fn( + async () => + new Map([ + [ + 'tom', + { + alive: false, + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + pidSource: 'process_table', + }, + ], + ]) + ); + const result = await svc.getMemberSpawnStatuses(teamName); + + expect(result.statuses.tom).toMatchObject({ + status: 'error', + launchState: 'failed_to_start', + bootstrapConfirmed: true, + runtimeAlive: false, + livenessKind: 'not_found', + hardFailure: true, + hardFailureReason: reason, + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }); + expect(result.teamLaunchState).toBe('partial_failure'); + }); + + it('keeps provisioned-but-not-alive failed when refreshed runtime evidence is only a candidate', async () => { + const teamName = 'primary-bootstrap-cli-provisioned-not-alive-runtime-candidate'; + const bootstrapRunId = 'run-primary-cli-exit-runtime-candidate'; + const reason = + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable'; + writeTeamMeta(teamName, { + providerId: 'anthropic', + model: 'sonnet', + }); + writeMembersMeta(teamName, [{ name: 'tom', providerId: 'anthropic', model: 'sonnet' }]); + writeLaunchConfig(teamName, '/Users/test/proj', 'lead-session', ['tom']); + writeMemberBootstrapRunId(teamName, 'tom', bootstrapRunId); + writeBootstrapState( + teamName, + [ + { + name: 'tom', + status: 'bootstrap_confirmed', + lastAttemptAt: Date.parse('2026-05-25T20:13:46.326Z'), + lastObservedAt: Date.parse('2026-05-25T20:13:56.110Z'), + }, + ], + '2026-05-25T20:14:03.317Z', + { runId: bootstrapRunId } + ); + fs.writeFileSync( + getTeamLaunchStatePath(teamName), + `${JSON.stringify( + createPersistedLaunchSnapshot({ + teamName, + leadSessionId: 'lead-session', + launchPhase: 'finished', + expectedMembers: ['tom'], + members: { + tom: { + name: 'tom', + providerId: 'anthropic', + model: 'sonnet', + laneId: 'primary', + laneKind: 'primary', + laneOwnerProviderId: 'anthropic', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + runtimePid: 27_036, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: reason, + livenessKind: 'registered_only', + pidSource: 'persisted_metadata', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + runtimeLastSeenAt: '2026-05-25T20:13:46.326Z', + lastEvaluatedAt: '2026-05-25T20:14:05.411Z', + }, + }, + updatedAt: '2026-05-25T20:14:05.411Z', + }), + null, + 2 + )}\n`, + 'utf8' + ); + + const svc = new TeamProvisioningService(); + (svc as any).getLiveTeamAgentRuntimeMetadata = vi.fn( + async () => + new Map([ + [ + 'tom', + { + alive: false, + livenessKind: 'runtime_process_candidate', + runtimeDiagnostic: + 'OpenCode runtime process detected, but teammate bootstrap is not confirmed', + runtimeDiagnosticSeverity: 'warning', + pidSource: 'opencode_bridge', + }, + ], + ]) + ); + const result = await svc.getMemberSpawnStatuses(teamName); + + expect(result.statuses.tom).toMatchObject({ + status: 'error', + launchState: 'failed_to_start', + bootstrapConfirmed: true, + runtimeAlive: false, + livenessKind: 'runtime_process_candidate', + hardFailure: true, + hardFailureReason: reason, + runtimeDiagnostic: + 'OpenCode runtime process detected, but teammate bootstrap is not confirmed', + runtimeDiagnosticSeverity: 'warning', + }); + expect(result.teamLaunchState).toBe('partial_failure'); + }); + it('cleans stale confirmed primary diagnostics from an already successful mixed launch', async () => { const teamName = 'mixed-confirmed-primary-stale-diagnostic-cleans'; writeTeamMeta(teamName, { @@ -27160,9 +27790,7 @@ describe('TeamProvisioningService', () => { model: 'claude-opus-4-7', members: [], }; - run.effectiveMembers = [ - { name: 'jack', providerId: 'anthropic', model: 'claude-opus-4-7' }, - ]; + run.effectiveMembers = [{ name: 'jack', providerId: 'anthropic', model: 'claude-opus-4-7' }]; fs.mkdirSync(path.join(tempTeamsBase, teamName), { recursive: true }); writeBootstrapState( teamName, @@ -27188,11 +27816,11 @@ describe('TeamProvisioningService', () => { ).persistLaunchStateSnapshot(run, 'finished'); expect(snapshot).toBeNull(); - await expect(fsPromises.readFile(getTeamLaunchStatePath(teamName), 'utf8')).rejects.toMatchObject( - { - code: 'ENOENT', - } - ); + await expect( + fsPromises.readFile(getTeamLaunchStatePath(teamName), 'utf8') + ).rejects.toMatchObject({ + code: 'ENOENT', + }); }); it('includes queued OpenCode secondary lanes in live spawn statuses during createTeam runs', async () => { diff --git a/test/renderer/components/team/members/MemberCard.test.ts b/test/renderer/components/team/members/MemberCard.test.ts index 10e43148..c0c0fd04 100644 --- a/test/renderer/components/team/members/MemberCard.test.ts +++ b/test/renderer/components/team/members/MemberCard.test.ts @@ -1217,6 +1217,55 @@ describe('MemberCard starting-state visuals', () => { }); }); + it('keeps stopped provisioned-but-not-alive launches failed and retryable', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + const reason = 'CLI process exited (code 1) - team provisioned but not alive'; + const spawnEntry: MemberSpawnStatusEntry = { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: reason, + agentToolAccepted: true, + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }; + + await act(async () => { + root.render( + React.createElement(MemberCard, { + member, + memberColor: 'blue', + isTeamAlive: true, + isTeamProvisioning: false, + spawnStatus: 'error', + spawnLaunchState: 'failed_to_start', + spawnRuntimeAlive: false, + spawnError: reason, + spawnEntry, + onRestartMember: vi.fn(), + onSkipMemberForLaunch: vi.fn(), + }) + ); + await Promise.resolve(); + }); + + expect(host.querySelector('[data-testid="member-launch-failure-reason"]')).not.toBeNull(); + expect(host.querySelector('[aria-label="Retry teammate"]')).not.toBeNull(); + expect(host.querySelector('[aria-label="Skip for this launch"]')).not.toBeNull(); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + it('shows a compact failed launch reason on the member row with clickable links', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); diff --git a/test/renderer/components/team/members/MemberDetailDialog.test.ts b/test/renderer/components/team/members/MemberDetailDialog.test.ts index 2cbeddaa..6d6ce4e7 100644 --- a/test/renderer/components/team/members/MemberDetailDialog.test.ts +++ b/test/renderer/components/team/members/MemberDetailDialog.test.ts @@ -531,6 +531,67 @@ describe('MemberDetailDialog activity count', () => { }); }); + it('shows Relaunch OpenCode copy for unsafe provisioned-but-not-alive OpenCode teammates without runtime evidence', async () => { + const member: ResolvedTeamMember = { + name: 'jack', + status: 'active', + currentTaskId: null, + taskCount: 0, + lastActiveAt: null, + messageCount: 0, + providerId: 'opencode', + }; + const onRestartMember = vi.fn(() => Promise.resolve(undefined)); + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + React.createElement(MemberDetailDialog, { + open: true, + member, + teamName: 'demo-team', + members: [member], + tasks: [], + isTeamAlive: true, + spawnEntry: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + agentToolAccepted: true, + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-04-24T12:00:00.000Z', + }, + onClose: () => undefined, + onSendMessage: () => undefined, + onAssignTask: () => undefined, + onTaskClick: () => undefined, + onRestartMember, + }) + ); + await Promise.resolve(); + }); + + expect(host.textContent).toContain( + 'No OpenCode runtime session was recorded. Relaunch this teammate to start a fresh OpenCode session.' + ); + const relaunchButton = Array.from(host.querySelectorAll('button')).find((button) => + button.textContent?.includes('Relaunch OpenCode') + ); + expect(relaunchButton).not.toBeUndefined(); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + it('shows Relaunch OpenCode copy for stalled OpenCode bootstrap', async () => { const member: ResolvedTeamMember = { name: 'tom', diff --git a/test/renderer/components/team/members/MemberList.test.ts b/test/renderer/components/team/members/MemberList.test.ts index 6f5528de..f2bcb0d9 100644 --- a/test/renderer/components/team/members/MemberList.test.ts +++ b/test/renderer/components/team/members/MemberList.test.ts @@ -3,7 +3,12 @@ import { createRoot } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { MemberSpawnStatusEntry, ResolvedTeamMember, TeamTaskWithKanban } from '@shared/types'; +import type { + MemberSpawnStatusEntry, + ResolvedTeamMember, + TeamAgentRuntimeEntry, + TeamTaskWithKanban, +} from '@shared/types'; vi.mock('@renderer/components/team/members/MemberCard', () => ({ MemberCard: ({ @@ -115,6 +120,19 @@ function offlineSpawnStatus(): MemberSpawnStatusEntry { }; } +function provisionedButNotAliveSpawnStatus(): MemberSpawnStatusEntry { + return { + status: 'error', + launchState: 'failed_to_start', + updatedAt: '2026-05-25T20:14:02.147Z', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + }; +} + function activeTask(id = 'task-active'): TeamTaskWithKanban { return { id, @@ -543,6 +561,142 @@ describe('MemberList spawn-status memoization', () => { }); }); + it('keeps tasks visible and suppresses launch actions for healed provisioned-but-not-alive status', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + const task = activeTask(); + const members: ResolvedTeamMember[] = [{ ...member, currentTaskId: task.id }]; + const restart = vi.fn(); + const skip = vi.fn(); + + await act(async () => { + root.render( + React.createElement(MemberList, { + members, + isTeamAlive: true, + taskMap: new Map([[task.id, task]]), + memberSpawnStatuses: new Map([['bob', provisionedButNotAliveSpawnStatus()]]), + memberRuntimeEntries: new Map([ + [ + 'bob', + { + memberName: 'bob', + alive: false, + restartable: true, + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:05.411Z', + }, + ], + ]), + onRestartMember: restart, + onSkipMemberForLaunch: skip, + }) + ); + await Promise.resolve(); + }); + + expect(host.querySelector('[data-testid="current-bob"]')?.textContent).toBe(task.id); + expect(host.querySelector('[data-testid="retry-bob"]')).toBeNull(); + expect(host.querySelector('[data-testid="skip-bob"]')).toBeNull(); + expect(host.textContent).not.toContain('team provisioned but not alive'); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + + it('keeps stopped provisioned-but-not-alive status failed and actionable', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + const task = activeTask(); + const members: ResolvedTeamMember[] = [{ ...member, currentTaskId: task.id }]; + const restart = vi.fn(); + const skip = vi.fn(); + const spawnEntry = { + ...provisionedButNotAliveSpawnStatus(), + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + } satisfies MemberSpawnStatusEntry; + + await act(async () => { + root.render( + React.createElement(MemberList, { + members, + isTeamAlive: true, + taskMap: new Map([[task.id, task]]), + memberSpawnStatuses: new Map([['bob', spawnEntry]]), + onRestartMember: restart, + onSkipMemberForLaunch: skip, + }) + ); + await Promise.resolve(); + }); + + expect(host.querySelector('[data-testid="current-bob"]')).toBeNull(); + expect(host.querySelector('[data-testid="retry-bob"]')).not.toBeNull(); + expect(host.querySelector('[data-testid="skip-bob"]')).not.toBeNull(); + expect(host.textContent).toContain('team provisioned but not alive'); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + + it('hides tasks for healed provisioned-but-not-alive status when runtime has an error', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + const task = activeTask(); + const members: ResolvedTeamMember[] = [{ ...member, currentTaskId: task.id }]; + + await act(async () => { + root.render( + React.createElement(MemberList, { + members, + isTeamAlive: true, + taskMap: new Map([[task.id, task]]), + memberSpawnStatuses: new Map([['bob', provisionedButNotAliveSpawnStatus()]]), + memberRuntimeEntries: new Map([ + [ + 'bob', + { + memberName: 'bob', + alive: false, + restartable: true, + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + updatedAt: '2026-05-25T20:14:05.411Z', + }, + ], + ]), + }) + ); + await Promise.resolve(); + }); + + expect(host.querySelector('[data-testid="current-bob"]')).toBeNull(); + expect(host.querySelector('[data-testid="retry-bob"]')).toBeNull(); + expect(host.querySelector('[data-testid="skip-bob"]')).toBeNull(); + expect(host.textContent).toContain('team provisioned but not alive'); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + it('passes skip callbacks to failed member cards and rerenders when the callback changes', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); diff --git a/test/renderer/components/team/provisioningSteps.test.ts b/test/renderer/components/team/provisioningSteps.test.ts index 78d8158c..ed26b347 100644 --- a/test/renderer/components/team/provisioningSteps.test.ts +++ b/test/renderer/components/team/provisioningSteps.test.ts @@ -144,6 +144,208 @@ describe('getLaunchJoinMilestonesFromMembers', () => { expect(milestones.pendingSpawnCount).toBe(3); }); + it('counts bootstrap-confirmed provisioned-but-not-alive entries as joined', () => { + const milestones = getLaunchJoinMilestonesFromMembers({ + members: [{ name: 'tom' }], + memberSpawnStatuses: { + tom: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + memberRuntimeEntries: { + tom: { + memberName: 'tom', + alive: false, + restartable: true, + livenessKind: 'registered_only', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:03.317Z', + }, + }, + }); + + expect(milestones.heartbeatConfirmedCount).toBe(1); + expect(milestones.failedSpawnCount).toBe(0); + expect(milestones.pendingSpawnCount).toBe(0); + }); + + it('uses spawn process-table proof when runtime registered metadata has no diagnostic text', () => { + const milestones = getLaunchJoinMilestonesFromMembers({ + members: [{ name: 'tom' }], + memberSpawnStatuses: { + tom: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + memberRuntimeEntries: { + tom: { + memberName: 'tom', + alive: false, + restartable: true, + livenessKind: 'registered_only', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:03.317Z', + }, + }, + }); + + expect(milestones.heartbeatConfirmedCount).toBe(1); + expect(milestones.failedSpawnCount).toBe(0); + expect(milestones.pendingSpawnCount).toBe(0); + }); + + it('uses spawn process-table proof when runtime metadata has no liveness or diagnostic text', () => { + const milestones = getLaunchJoinMilestonesFromMembers({ + members: [{ name: 'tom' }], + memberSpawnStatuses: { + tom: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + memberRuntimeEntries: { + tom: { + memberName: 'tom', + alive: false, + restartable: true, + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:03.317Z', + }, + }, + }); + + expect(milestones.heartbeatConfirmedCount).toBe(1); + expect(milestones.failedSpawnCount).toBe(0); + expect(milestones.pendingSpawnCount).toBe(0); + }); + + it('counts unsafe bootstrap-confirmed provisioned-but-not-alive entries as failed', () => { + const milestones = getLaunchJoinMilestonesFromMembers({ + members: [{ name: 'tom' }], + memberSpawnStatuses: { + tom: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + }); + + expect(milestones.heartbeatConfirmedCount).toBe(0); + expect(milestones.failedSpawnCount).toBe(1); + expect(milestones.pendingSpawnCount).toBe(0); + }); + + it('keeps ambiguous runtime-offline entries pending even when provisioned-but-not-alive spawn evidence is safe', () => { + const milestones = getLaunchJoinMilestonesFromMembers({ + members: [{ name: 'tom' }], + memberSpawnStatuses: { + tom: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + livenessKind: 'registered_only', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + memberRuntimeEntries: { + tom: { + memberName: 'tom', + alive: false, + restartable: true, + runtimeDiagnostic: 'Runtime heartbeat is not alive', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:03.317Z', + }, + }, + }); + + expect(milestones.heartbeatConfirmedCount).toBe(0); + expect(milestones.failedSpawnCount).toBe(0); + expect(milestones.pendingSpawnCount).toBe(1); + }); + + it('does not count safe provisioned-but-not-alive spawn evidence as joined when live runtime evidence is an error', () => { + const milestones = getLaunchJoinMilestonesFromMembers({ + members: [{ name: 'tom' }], + memberSpawnStatuses: { + tom: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + memberRuntimeEntries: { + tom: { + memberName: 'tom', + alive: false, + restartable: true, + livenessKind: 'registered_only', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'error', + updatedAt: '2026-05-25T20:14:03.317Z', + }, + }, + }); + + expect(milestones.heartbeatConfirmedCount).toBe(0); + expect(milestones.failedSpawnCount).toBe(0); + expect(milestones.pendingSpawnCount).toBe(1); + }); + it('does not let a stale clean snapshot hide live registered-only members', () => { const milestones = getLaunchJoinMilestonesFromMembers({ members, @@ -243,4 +445,131 @@ describe('getLaunchJoinMilestonesFromMembers', () => { expect(milestones.pendingSpawnCount).toBe(1); expect(milestones.expectedTeammateCount).toBe(4); }); + + it('does not count confirmed spawn as joined when spawn metadata carries runtime error evidence', () => { + const milestones = getLaunchJoinMilestonesFromMembers({ + members, + memberSpawnStatuses: { + alice: { + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: true, + bootstrapConfirmed: true, + updatedAt: '2026-04-24T12:00:00.000Z', + }, + bob: { + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: false, + bootstrapConfirmed: true, + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + updatedAt: '2026-04-24T12:00:01.000Z', + }, + tom: { + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: true, + bootstrapConfirmed: true, + updatedAt: '2026-04-24T12:00:00.000Z', + }, + jane: { + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: true, + bootstrapConfirmed: true, + updatedAt: '2026-04-24T12:00:00.000Z', + }, + }, + }); + + expect(milestones.heartbeatConfirmedCount).toBe(3); + expect(milestones.pendingSpawnCount).toBe(1); + expect(milestones.expectedTeammateCount).toBe(4); + }); + + it('does not count confirmed spawn as joined when stopped spawn metadata has no liveness kind', () => { + const milestones = getLaunchJoinMilestonesFromMembers({ + members, + memberSpawnStatuses: { + alice: { + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: true, + bootstrapConfirmed: true, + updatedAt: '2026-04-24T12:00:00.000Z', + }, + bob: { + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: false, + bootstrapConfirmed: true, + updatedAt: '2026-04-24T12:00:01.000Z', + }, + tom: { + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: true, + bootstrapConfirmed: true, + updatedAt: '2026-04-24T12:00:00.000Z', + }, + jane: { + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: true, + bootstrapConfirmed: true, + updatedAt: '2026-04-24T12:00:00.000Z', + }, + }, + }); + + expect(milestones.heartbeatConfirmedCount).toBe(3); + expect(milestones.pendingSpawnCount).toBe(1); + expect(milestones.expectedTeammateCount).toBe(4); + }); + + it('counts process-table-unavailable provisioned-but-not-alive spawn without liveness kind as joined', () => { + const milestones = getLaunchJoinMilestonesFromMembers({ + members, + memberSpawnStatuses: { + alice: { + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: true, + bootstrapConfirmed: true, + updatedAt: '2026-04-24T12:00:00.000Z', + }, + bob: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + updatedAt: '2026-04-24T12:00:01.000Z', + }, + tom: { + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: true, + bootstrapConfirmed: true, + updatedAt: '2026-04-24T12:00:00.000Z', + }, + jane: { + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: true, + bootstrapConfirmed: true, + updatedAt: '2026-04-24T12:00:00.000Z', + }, + }, + }); + + expect(milestones.heartbeatConfirmedCount).toBe(4); + expect(milestones.pendingSpawnCount).toBe(0); + expect(milestones.failedSpawnCount).toBe(0); + expect(milestones.expectedTeammateCount).toBe(4); + }); }); diff --git a/test/renderer/components/team/teamRuntimeDisplayRows.test.ts b/test/renderer/components/team/teamRuntimeDisplayRows.test.ts index 8ac8c479..a4c564bf 100644 --- a/test/renderer/components/team/teamRuntimeDisplayRows.test.ts +++ b/test/renderer/components/team/teamRuntimeDisplayRows.test.ts @@ -30,7 +30,9 @@ function createRuntimeSnapshot( }; } -function createSpawnStatus(overrides: Partial = {}): MemberSpawnStatusEntry { +function createSpawnStatus( + overrides: Partial = {} +): MemberSpawnStatusEntry { return { status: 'spawning', launchState: 'starting', @@ -251,6 +253,274 @@ describe('buildTeamRuntimeDisplayRows', () => { }); }); + it('does not degrade bootstrap-confirmed provisioned-but-not-alive rows', () => { + const rows = buildTeamRuntimeDisplayRows({ + members: [{ name: 'alice' }], + runtimeSnapshot: createRuntimeSnapshot({ + alice: createRuntimeEntry({ + alive: false, + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + }), + }), + spawnStatuses: { + alice: createSpawnStatus({ + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + }), + }, + }); + + expect(rows[0]).toMatchObject({ + memberName: 'alice', + state: 'running', + source: 'mixed', + stateReason: 'Bootstrap confirmed', + diagnosticSeverity: 'warning', + actionsAllowed: false, + }); + }); + + it('does not degrade Windows process-table-unavailable registered metadata rows', () => { + const rows = buildTeamRuntimeDisplayRows({ + members: [{ name: 'alice' }], + runtimeSnapshot: createRuntimeSnapshot({ + alice: createRuntimeEntry({ + alive: false, + livenessKind: 'registered_only', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + }), + }), + spawnStatuses: { + alice: createSpawnStatus({ + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + livenessKind: 'registered_only', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + }), + }, + }); + + expect(rows[0]).toMatchObject({ + memberName: 'alice', + state: 'running', + source: 'mixed', + stateReason: 'Bootstrap confirmed', + diagnosticSeverity: 'warning', + actionsAllowed: false, + }); + }); + + it('uses spawn process-table proof when runtime registered metadata has no diagnostic text', () => { + const rows = buildTeamRuntimeDisplayRows({ + members: [{ name: 'alice' }], + runtimeSnapshot: createRuntimeSnapshot({ + alice: createRuntimeEntry({ + alive: false, + livenessKind: 'registered_only', + runtimeDiagnosticSeverity: 'warning', + }), + }), + spawnStatuses: { + alice: createSpawnStatus({ + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + livenessKind: 'confirmed_bootstrap', + }), + }, + }); + + expect(rows[0]).toMatchObject({ + memberName: 'alice', + state: 'running', + source: 'mixed', + stateReason: 'Bootstrap confirmed', + diagnosticSeverity: 'warning', + actionsAllowed: false, + }); + }); + + it('does not let stale provisioned-but-not-alive spawn evidence hide runtime errors', () => { + const rows = buildTeamRuntimeDisplayRows({ + members: [{ name: 'alice' }], + runtimeSnapshot: createRuntimeSnapshot({ + alice: createRuntimeEntry({ + alive: false, + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }), + }), + spawnStatuses: { + alice: createSpawnStatus({ + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + }), + }, + }); + + expect(rows[0]).toMatchObject({ + memberName: 'alice', + state: 'degraded', + source: 'mixed', + stateReason: 'Runtime process crashed', + diagnosticSeverity: 'error', + actionsAllowed: false, + }); + }); + + it('does not let provisioned-but-not-alive spawn evidence hide stopped runtime evidence', () => { + const rows = buildTeamRuntimeDisplayRows({ + members: [{ name: 'alice' }], + runtimeSnapshot: createRuntimeSnapshot({ + alice: createRuntimeEntry({ + alive: false, + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime metadata was not found', + runtimeDiagnosticSeverity: 'warning', + }), + }), + spawnStatuses: { + alice: createSpawnStatus({ + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + }), + }, + }); + + expect(rows[0]).toMatchObject({ + memberName: 'alice', + state: 'stopped', + source: 'mixed', + stateReason: 'Runtime metadata was not found', + diagnosticSeverity: 'warning', + actionsAllowed: false, + }); + }); + + it('does not let stopped provisioned-but-not-alive spawn evidence hide live runtime context', () => { + const rows = buildTeamRuntimeDisplayRows({ + members: [{ name: 'alice' }], + runtimeSnapshot: createRuntimeSnapshot({ + alice: createRuntimeEntry({ + alive: true, + livenessKind: 'runtime_process', + runtimeDiagnostic: 'Runtime process is alive', + runtimeDiagnosticSeverity: 'info', + }), + }), + spawnStatuses: { + alice: createSpawnStatus({ + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + }), + }, + }); + + expect(rows[0]).toMatchObject({ + memberName: 'alice', + state: 'degraded', + source: 'mixed', + stateReason: 'Runtime is no longer registered. Process is still alive.', + diagnosticSeverity: 'warning', + actionsAllowed: false, + }); + }); + + it('keeps spawn-only runtime errors visible for provisioned-but-not-alive entries', () => { + const rows = buildTeamRuntimeDisplayRows({ + members: [{ name: 'alice' }], + spawnStatuses: { + alice: createSpawnStatus({ + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }), + }, + }); + + expect(rows[0]).toMatchObject({ + memberName: 'alice', + state: 'degraded', + source: 'spawn-status', + stateReason: 'Runtime process crashed', + diagnosticSeverity: 'error', + actionsAllowed: false, + }); + }); + + it('keeps spawn-only stopped liveness visible for provisioned-but-not-alive entries', () => { + const rows = buildTeamRuntimeDisplayRows({ + members: [{ name: 'alice' }], + spawnStatuses: { + alice: createSpawnStatus({ + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + }), + }, + }); + + expect(rows[0]).toMatchObject({ + memberName: 'alice', + state: 'degraded', + source: 'spawn-status', + stateReason: 'Runtime is no longer registered', + diagnosticSeverity: 'warning', + actionsAllowed: false, + }); + }); + it('degrades spawn-only rows when online process evidence has stalled bootstrap', () => { const rows = buildTeamRuntimeDisplayRows({ members: [{ name: 'alice' }], diff --git a/test/renderer/features/agent-graph/TeamGraphAdapter.test.ts b/test/renderer/features/agent-graph/TeamGraphAdapter.test.ts index 1d3d1977..99fd1cf8 100644 --- a/test/renderer/features/agent-graph/TeamGraphAdapter.test.ts +++ b/test/renderer/features/agent-graph/TeamGraphAdapter.test.ts @@ -1777,6 +1777,154 @@ describe('TeamGraphAdapter particles', () => { }); }); + it('keeps bootstrap-confirmed spawn diagnostic errors in graph error state', () => { + const adapter = TeamGraphAdapter.create(); + const graph = adapter.adapt(createBaseTeamData(), 'my-team', { + alice: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }); + + expect(findNode(graph, 'member:my-team:alice')).toMatchObject({ + state: 'error', + spawnStatus: 'error', + launchVisualState: 'error', + launchStatusLabel: 'failed', + exceptionTone: 'error', + exceptionLabel: 'spawn failed', + }); + }); + + it('keeps bootstrap-confirmed stopped runtime evidence in graph error state', () => { + const adapter = TeamGraphAdapter.create(); + const graph = adapter.adapt(createBaseTeamData(), 'my-team', { + alice: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }); + + expect(findNode(graph, 'member:my-team:alice')).toMatchObject({ + state: 'error', + spawnStatus: 'error', + launchVisualState: 'error', + launchStatusLabel: 'failed', + exceptionTone: 'error', + exceptionLabel: 'spawn failed', + }); + }); + + it('uses spawn process-table proof when graph runtime metadata has no diagnostic text', () => { + const adapter = TeamGraphAdapter.create(); + const graph = adapter.adapt( + createBaseTeamData({ + runtimeEntriesByMember: { + alice: createLiveRuntimeEntry('alice', { + alive: false, + livenessKind: 'registered_only', + runtimeDiagnosticSeverity: 'warning', + }), + }, + }), + 'my-team', + { + alice: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + } + ); + + expect(findNode(graph, 'member:my-team:alice')).toMatchObject({ + state: 'active', + spawnStatus: 'error', + launchVisualState: undefined, + launchStatusLabel: undefined, + exceptionTone: undefined, + exceptionLabel: undefined, + }); + }); + + it.each([ + { + name: 'runtime diagnostic error', + runtime: { + alive: false, + livenessKind: 'runtime_process', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }, + }, + { + name: 'stopped runtime liveness', + runtime: { + alive: false, + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + }, + }, + ] as const)( + 'keeps graph errors when live runtime has unsafe evidence for a safe bootstrap-confirmed spawn: $name', + ({ runtime }) => { + const adapter = TeamGraphAdapter.create(); + const graph = adapter.adapt( + createBaseTeamData({ + runtimeEntriesByMember: { + alice: createLiveRuntimeEntry('alice', runtime), + }, + }), + 'my-team', + { + alice: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + } + ); + + expect(findNode(graph, 'member:my-team:alice')).toMatchObject({ + state: 'error', + spawnStatus: 'error', + exceptionTone: 'error', + exceptionLabel: 'spawn failed', + }); + } + ); + it('treats permission-blocked spawn state as awaiting approval even without pending approval feed', () => { const adapter = TeamGraphAdapter.create(); const teamData = createBaseTeamData(); diff --git a/test/renderer/store/teamSlice.test.ts b/test/renderer/store/teamSlice.test.ts index 7351801c..ffd422fe 100644 --- a/test/renderer/store/teamSlice.test.ts +++ b/test/renderer/store/teamSlice.test.ts @@ -419,6 +419,8 @@ describe('teamSlice actions', () => { expect(fetchTeams).toHaveBeenCalledTimes(1); expect(refreshTeamData).toHaveBeenCalledTimes(1); expect(refreshTeamData).toHaveBeenCalledWith('my-team', { withDedup: true }); + expect(hoisted.getMemberSpawnStatuses).toHaveBeenCalledWith('my-team'); + expect(hoisted.getTeamAgentRuntime).toHaveBeenCalledWith('my-team'); const snapshot = getTeamRefreshFanoutSnapshotForTests( 'my-team' @@ -431,6 +433,16 @@ describe('teamSlice actions', () => { 'provisioning-progress:provisioning:terminal-ready:refreshTeamData:scheduled' ] ).toBe(1); + expect( + snapshot?.counts[ + 'provisioning-progress:provisioning:terminal-ready:fetchMemberSpawnStatuses:scheduled' + ] + ).toBe(1); + expect( + snapshot?.counts[ + 'provisioning-progress:provisioning:terminal-ready:fetchTeamAgentRuntime:scheduled' + ] + ).toBe(1); }); it('maps inbox verify failure to user-friendly text', async () => { @@ -6396,6 +6408,84 @@ describe('teamSlice actions', () => { ); }); + it('refreshes retained terminal spawn errors after disconnected progress', async () => { + const store = createSliceStore(); + const startedAt = '2026-03-12T10:00:00.000Z'; + const staleReason = 'CLI process exited (code 1) \u2014 team provisioned but not alive'; + store.setState({ + selectedTeamName: 'my-team', + selectedTeamData: createTeamSnapshot(), + paneLayout: { + focusedPaneId: 'pane-default', + panes: [ + { + id: 'pane-default', + widthFraction: 1, + tabs: [{ id: 'team-my-team', type: 'team', teamName: 'my-team', label: 'My Team' }], + activeTabId: 'team-my-team', + }, + ], + }, + currentProvisioningRunIdByTeam: { + 'my-team': 'run-current', + }, + currentRuntimeRunIdByTeam: { + 'my-team': 'run-current', + }, + memberSpawnStatusesByTeam: { + 'my-team': { + tom: createMemberSpawnStatus({ + status: 'error', + launchState: 'failed_to_start', + error: staleReason, + hardFailure: true, + hardFailureReason: staleReason, + bootstrapConfirmed: true, + runtimeAlive: false, + }), + }, + }, + }); + hoisted.getMemberSpawnStatuses.mockResolvedValue( + createMemberSpawnSnapshot({ + runId: 'run-current', + expectedMembers: ['tom'], + statuses: { + tom: createMemberSpawnStatus({ + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: false, + livenessKind: 'confirmed_bootstrap', + hardFailure: false, + hardFailureReason: undefined, + error: undefined, + }), + }, + }) + ); + + store.getState().onProvisioningProgress({ + runId: 'run-current', + teamName: 'my-team', + state: 'disconnected', + message: 'Disconnected', + startedAt, + updatedAt: '2026-03-12T10:00:01.000Z', + }); + + await vi.waitFor(() => { + expect(store.getState().memberSpawnStatusesByTeam['my-team']?.tom).toMatchObject({ + status: 'online', + launchState: 'confirmed_alive', + bootstrapConfirmed: true, + hardFailure: false, + }); + }); + expect( + store.getState().memberSpawnStatusesByTeam['my-team']?.tom?.hardFailureReason + ).toBeUndefined(); + }); + it('does not fall back to a team-wide latest run when no current run is pinned', () => { expect( getCurrentProvisioningProgressForTeam( diff --git a/test/renderer/utils/memberHelpers.test.ts b/test/renderer/utils/memberHelpers.test.ts index 9c559fa5..b717d5f0 100644 --- a/test/renderer/utils/memberHelpers.test.ts +++ b/test/renderer/utils/memberHelpers.test.ts @@ -11,7 +11,11 @@ import { shouldDisplayMemberCurrentTask, } from '@renderer/utils/memberHelpers'; -import type { ResolvedTeamMember } from '@shared/types'; +import type { + MemberSpawnStatusEntry, + ResolvedTeamMember, + TeamAgentRuntimeEntry, +} from '@shared/types'; const member: ResolvedTeamMember = { name: 'alice', @@ -27,6 +31,28 @@ const member: ResolvedTeamMember = { removedAt: undefined, }; +const provisionedButNotAliveSpawn: MemberSpawnStatusEntry = { + status: 'error', + launchState: 'failed_to_start', + updatedAt: '2026-05-25T20:14:02.147Z', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', +}; + +const processTableUnavailableRuntime: TeamAgentRuntimeEntry = { + memberName: 'alice', + alive: false, + restartable: true, + providerId: 'anthropic', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:05.411Z', +}; + describe('memberHelpers spawn-aware presence', () => { it('does not display current task labels for offline or terminal launch states', () => { expect( @@ -121,6 +147,92 @@ describe('memberHelpers spawn-aware presence', () => { ).toBe(true); }); + it('treats bootstrap-confirmed provisioned-but-not-alive entries as active for task display', () => { + expect( + shouldDisplayMemberCurrentTask({ + member: { ...member, currentTaskId: 'task-1' }, + isTeamAlive: true, + spawnStatus: provisionedButNotAliveSpawn.status, + spawnLaunchState: provisionedButNotAliveSpawn.launchState, + spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive, + spawnEntry: provisionedButNotAliveSpawn, + runtimeEntry: processTableUnavailableRuntime, + }) + ).toBe(true); + }); + + it('treats spawn-only bootstrap-confirmed provisioned-but-not-alive entries as active for task display', () => { + expect( + shouldDisplayMemberCurrentTask({ + member: { ...member, currentTaskId: 'task-1' }, + isTeamAlive: true, + spawnStatus: provisionedButNotAliveSpawn.status, + spawnLaunchState: provisionedButNotAliveSpawn.launchState, + spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive, + spawnEntry: provisionedButNotAliveSpawn, + }) + ).toBe(true); + }); + + it('does not show task activity for provisioned-but-not-alive entries with runtime errors', () => { + expect( + shouldDisplayMemberCurrentTask({ + member: { ...member, currentTaskId: 'task-1' }, + isTeamAlive: true, + spawnStatus: provisionedButNotAliveSpawn.status, + spawnLaunchState: provisionedButNotAliveSpawn.launchState, + spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive, + spawnEntry: provisionedButNotAliveSpawn, + runtimeEntry: { + ...processTableUnavailableRuntime, + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }, + }) + ).toBe(false); + + expect( + shouldDisplayMemberCurrentTask({ + member: { ...member, currentTaskId: 'task-1' }, + isTeamAlive: true, + spawnStatus: provisionedButNotAliveSpawn.status, + spawnLaunchState: provisionedButNotAliveSpawn.launchState, + spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive, + spawnEntry: { + ...provisionedButNotAliveSpawn, + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }, + runtimeEntry: processTableUnavailableRuntime, + }) + ).toBe(false); + }); + + it('does not show task activity for unsafe provisioned-but-not-alive runtime candidates', () => { + expect( + shouldDisplayMemberCurrentTask({ + member: { ...member, currentTaskId: 'task-1' }, + isTeamAlive: true, + spawnStatus: 'online', + spawnLaunchState: 'confirmed_alive', + spawnRuntimeAlive: true, + spawnEntry: { + ...provisionedButNotAliveSpawn, + status: 'online', + launchState: 'confirmed_alive', + runtimeAlive: true, + }, + runtimeEntry: { + ...processTableUnavailableRuntime, + alive: false, + livenessKind: 'runtime_process_candidate', + runtimeDiagnostic: + 'OpenCode runtime process detected, but teammate bootstrap is not confirmed', + }, + }) + ).toBe(false); + }); + it('shows process-online teammates as online with a green dot', () => { expect( getSpawnAwarePresenceLabel( @@ -657,6 +769,44 @@ describe('memberHelpers spawn-aware presence', () => { ).toBe(true); }); + it('marks unsafe provisioned-but-not-alive OpenCode entries as relaunchable', () => { + expect( + isOpenCodeRelaunchActionable({ + member: { ...member, providerId: 'opencode' }, + spawnEntry: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }) + ).toBe(true); + + expect( + isOpenCodeRelaunchActionable({ + member: { ...member, providerId: 'opencode' }, + spawnEntry: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }) + ).toBe(true); + }); + it('does not mark fresh OpenCode runtime candidates as relaunchable', () => { expect( isOpenCodeRelaunchActionable({ @@ -780,6 +930,214 @@ describe('memberHelpers spawn-aware presence', () => { }); }); + it('does not render bootstrap-confirmed provisioned-but-not-alive entries as failed or stale', () => { + expect( + buildMemberLaunchPresentation({ + member, + spawnStatus: provisionedButNotAliveSpawn.status, + spawnLaunchState: provisionedButNotAliveSpawn.launchState, + spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource, + spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive, + spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed, + spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled, + spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted, + spawnHardFailure: provisionedButNotAliveSpawn.hardFailure, + spawnHardFailureReason: provisionedButNotAliveSpawn.hardFailureReason, + spawnError: provisionedButNotAliveSpawn.error, + spawnLivenessKind: provisionedButNotAliveSpawn.livenessKind, + runtimeEntry: processTableUnavailableRuntime, + runtimeAdvisory: undefined, + isLaunchSettling: false, + isTeamAlive: true, + isTeamProvisioning: false, + }) + ).toMatchObject({ + presenceLabel: 'idle', + launchVisualState: null, + launchStatusLabel: null, + spawnBadgeLabel: null, + }); + }); + + it('does not render spawn-only bootstrap-confirmed provisioned-but-not-alive entries as failed or stale', () => { + expect( + buildMemberLaunchPresentation({ + member, + spawnStatus: provisionedButNotAliveSpawn.status, + spawnLaunchState: provisionedButNotAliveSpawn.launchState, + spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource, + spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive, + spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed, + spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled, + spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted, + spawnHardFailure: provisionedButNotAliveSpawn.hardFailure, + spawnHardFailureReason: provisionedButNotAliveSpawn.hardFailureReason, + spawnError: provisionedButNotAliveSpawn.error, + spawnLivenessKind: provisionedButNotAliveSpawn.livenessKind, + runtimeAdvisory: undefined, + isLaunchSettling: false, + isTeamAlive: true, + isTeamProvisioning: false, + }) + ).toMatchObject({ + presenceLabel: 'idle', + launchVisualState: null, + launchStatusLabel: null, + spawnBadgeLabel: null, + }); + }); + + it('does not leak safe process-table liveness into healed member visuals', () => { + expect( + buildMemberLaunchPresentation({ + member, + spawnStatus: provisionedButNotAliveSpawn.status, + spawnLaunchState: provisionedButNotAliveSpawn.launchState, + spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource, + spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive, + spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed, + spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled, + spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted, + spawnHardFailure: provisionedButNotAliveSpawn.hardFailure, + spawnHardFailureReason: provisionedButNotAliveSpawn.hardFailureReason, + spawnError: provisionedButNotAliveSpawn.error, + spawnLivenessKind: provisionedButNotAliveSpawn.livenessKind, + runtimeEntry: { + ...processTableUnavailableRuntime, + livenessKind: 'registered_only', + }, + runtimeAdvisory: undefined, + isLaunchSettling: false, + isTeamAlive: true, + isTeamProvisioning: false, + }) + ).toMatchObject({ + presenceLabel: 'idle', + launchVisualState: null, + launchStatusLabel: null, + spawnBadgeLabel: null, + }); + }); + + it('recognizes provisioned-but-not-alive when the reason is only in runtime diagnostics', () => { + expect( + buildMemberLaunchPresentation({ + member, + spawnStatus: provisionedButNotAliveSpawn.status, + spawnLaunchState: provisionedButNotAliveSpawn.launchState, + spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource, + spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive, + spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed, + spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled, + spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted, + spawnHardFailure: provisionedButNotAliveSpawn.hardFailure, + spawnRuntimeDiagnostic: provisionedButNotAliveSpawn.hardFailureReason, + spawnLivenessKind: provisionedButNotAliveSpawn.livenessKind, + runtimeEntry: processTableUnavailableRuntime, + runtimeAdvisory: undefined, + isLaunchSettling: false, + isTeamAlive: true, + isTeamProvisioning: false, + }) + ).toMatchObject({ + presenceLabel: 'idle', + launchVisualState: null, + launchStatusLabel: null, + spawnBadgeLabel: null, + }); + }); + + it('keeps runtime errors visible for bootstrap-confirmed provisioned-but-not-alive entries', () => { + expect( + buildMemberLaunchPresentation({ + member, + spawnStatus: provisionedButNotAliveSpawn.status, + spawnLaunchState: provisionedButNotAliveSpawn.launchState, + spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource, + spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive, + spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed, + spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled, + spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted, + spawnHardFailure: provisionedButNotAliveSpawn.hardFailure, + spawnHardFailureReason: provisionedButNotAliveSpawn.hardFailureReason, + spawnError: provisionedButNotAliveSpawn.error, + spawnLivenessKind: provisionedButNotAliveSpawn.livenessKind, + spawnRuntimeDiagnosticSeverity: provisionedButNotAliveSpawn.runtimeDiagnosticSeverity, + runtimeEntry: { + ...processTableUnavailableRuntime, + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }, + runtimeAdvisory: undefined, + isLaunchSettling: false, + isTeamAlive: true, + isTeamProvisioning: false, + }) + ).toMatchObject({ + presenceLabel: 'spawn failed', + launchVisualState: 'error', + launchStatusLabel: 'failed', + spawnBadgeLabel: 'error', + }); + }); + + it('keeps spawn diagnostic errors visible for bootstrap-confirmed provisioned-but-not-alive entries', () => { + expect( + buildMemberLaunchPresentation({ + member, + spawnStatus: provisionedButNotAliveSpawn.status, + spawnLaunchState: provisionedButNotAliveSpawn.launchState, + spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource, + spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive, + spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed, + spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled, + spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted, + spawnHardFailure: provisionedButNotAliveSpawn.hardFailure, + spawnHardFailureReason: provisionedButNotAliveSpawn.hardFailureReason, + spawnError: provisionedButNotAliveSpawn.error, + spawnLivenessKind: provisionedButNotAliveSpawn.livenessKind, + spawnRuntimeDiagnosticSeverity: 'error', + runtimeAdvisory: undefined, + isLaunchSettling: false, + isTeamAlive: true, + isTeamProvisioning: false, + }) + ).toMatchObject({ + presenceLabel: 'spawn failed', + launchVisualState: 'error', + launchStatusLabel: 'failed', + }); + }); + + it('keeps stopped runtime evidence failed for bootstrap-confirmed provisioned-but-not-alive entries', () => { + expect( + buildMemberLaunchPresentation({ + member, + spawnStatus: provisionedButNotAliveSpawn.status, + spawnLaunchState: provisionedButNotAliveSpawn.launchState, + spawnLivenessSource: provisionedButNotAliveSpawn.livenessSource, + spawnRuntimeAlive: provisionedButNotAliveSpawn.runtimeAlive, + spawnBootstrapConfirmed: provisionedButNotAliveSpawn.bootstrapConfirmed, + spawnBootstrapStalled: provisionedButNotAliveSpawn.bootstrapStalled, + spawnAgentToolAccepted: provisionedButNotAliveSpawn.agentToolAccepted, + spawnHardFailure: provisionedButNotAliveSpawn.hardFailure, + spawnHardFailureReason: provisionedButNotAliveSpawn.hardFailureReason, + spawnError: provisionedButNotAliveSpawn.error, + spawnLivenessKind: 'not_found', + spawnRuntimeDiagnosticSeverity: 'warning', + runtimeAdvisory: undefined, + isLaunchSettling: false, + isTeamAlive: true, + isTeamProvisioning: false, + }) + ).toMatchObject({ + presenceLabel: 'spawn failed', + launchVisualState: 'error', + launchStatusLabel: 'failed', + spawnBadgeLabel: 'error', + }); + }); + it('renders unified retry advisory labels for provider retries', () => { expect( getMemberRuntimeAdvisoryLabel( diff --git a/test/renderer/utils/memberLaunchDiagnostics.test.ts b/test/renderer/utils/memberLaunchDiagnostics.test.ts index 80b8ec8e..1117052e 100644 --- a/test/renderer/utils/memberLaunchDiagnostics.test.ts +++ b/test/renderer/utils/memberLaunchDiagnostics.test.ts @@ -1,5 +1,6 @@ import { buildMemberLaunchDiagnosticsPayload, + buildTeamMemberLaunchDiagnosticsPayloads, formatMemberLaunchDiagnosticsPayload, getMemberLaunchDiagnosticsErrorMessage, hasMemberLaunchDiagnosticsDetails, @@ -123,6 +124,360 @@ describe('member launch diagnostics', () => { expect(payload.runtimeDiagnostic).toBe('persisted runtime pid is not alive'); }); + it('does not surface bootstrap-confirmed provisioned-but-not-alive entries as card errors', () => { + const payload = buildMemberLaunchDiagnosticsPayload({ + teamName: 'signal-ops', + runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5', + memberName: 'tom', + spawnEntry: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + runtimeEntry: { + memberName: 'tom', + alive: false, + restartable: true, + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:03.317Z', + }, + }); + + expect(payload).toMatchObject({ + launchState: 'confirmed_alive', + spawnStatus: 'online', + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + }); + expect(payload.memberCardError).toBeUndefined(); + expect(payload.probableCause).toBeUndefined(); + expect(hasMemberLaunchDiagnosticsError(payload)).toBe(false); + expect(getMemberLaunchDiagnosticsErrorMessage(payload)).toBeUndefined(); + }); + + it('does not surface spawn-only safe bootstrap-confirmed provisioned-but-not-alive entries as card errors', () => { + const payload = buildMemberLaunchDiagnosticsPayload({ + teamName: 'signal-ops', + runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5', + memberName: 'tom', + spawnEntry: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }); + + expect(payload).toMatchObject({ + launchState: 'confirmed_alive', + spawnStatus: 'online', + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + }); + expect(payload.memberCardError).toBeUndefined(); + expect(hasMemberLaunchDiagnosticsError(payload)).toBe(false); + expect(getMemberLaunchDiagnosticsErrorMessage(payload)).toBeUndefined(); + }); + + it('keeps runtime errors visible for bootstrap-confirmed provisioned-but-not-alive entries', () => { + const payload = buildMemberLaunchDiagnosticsPayload({ + teamName: 'signal-ops', + runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5', + memberName: 'tom', + spawnEntry: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + runtimeEntry: { + memberName: 'tom', + alive: false, + restartable: true, + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + updatedAt: '2026-05-25T20:14:03.317Z', + }, + }); + + expect(payload).toMatchObject({ + launchState: 'failed_to_start', + spawnStatus: 'error', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + memberCardError: 'Runtime process crashed', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }); + expect(hasMemberLaunchDiagnosticsError(payload)).toBe(true); + expect(getMemberLaunchDiagnosticsErrorMessage(payload)).toBe('Runtime process crashed'); + }); + + it('keeps spawn errors visible when runtime evidence is only warning severity', () => { + const payload = buildMemberLaunchDiagnosticsPayload({ + teamName: 'signal-ops', + runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5', + memberName: 'tom', + spawnEntry: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + runtimeEntry: { + memberName: 'tom', + alive: false, + restartable: true, + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:03.317Z', + }, + }); + + expect(payload).toMatchObject({ + launchState: 'failed_to_start', + spawnStatus: 'error', + runtimeAlive: false, + hardFailure: true, + memberCardError: 'Runtime process crashed', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }); + expect(payload.diagnostics).toContain('Runtime process crashed'); + expect(hasMemberLaunchDiagnosticsError(payload)).toBe(true); + }); + + it('keeps spawn diagnostics for bootstrap-confirmed provisioned-but-not-alive entries without runtime evidence', () => { + const payload = buildMemberLaunchDiagnosticsPayload({ + teamName: 'signal-ops', + runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5', + memberName: 'tom', + spawnEntry: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }); + + expect(payload).toMatchObject({ + launchState: 'failed_to_start', + spawnStatus: 'error', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + memberCardError: 'Runtime process crashed', + runtimeDiagnostic: 'Runtime process crashed', + runtimeDiagnosticSeverity: 'error', + }); + expect(payload.diagnostics).toContain('Runtime process crashed'); + expect(hasMemberLaunchDiagnosticsError(payload)).toBe(true); + }); + + it('does not heal stopped liveness evidence for bootstrap-confirmed provisioned-but-not-alive entries', () => { + const payload = buildMemberLaunchDiagnosticsPayload({ + teamName: 'signal-ops', + runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5', + memberName: 'tom', + spawnEntry: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + firstSpawnAcceptedAt: '2026-05-25T20:13:46.326Z', + lastHeartbeatAt: '2026-05-25T20:13:56.110Z', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }); + + expect(payload).toMatchObject({ + launchState: 'failed_to_start', + spawnStatus: 'error', + runtimeAlive: false, + hardFailure: true, + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + }); + }); + + it('keeps unsafe spawn diagnostics over benign runtime warnings for provisioned-but-not-alive entries', () => { + const payload = buildMemberLaunchDiagnosticsPayload({ + teamName: 'signal-ops', + runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5', + memberName: 'tom', + spawnEntry: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + runtimeEntry: { + memberName: 'tom', + alive: false, + restartable: true, + livenessKind: 'registered_only', + runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:03.317Z', + }, + }); + + expect(payload).toMatchObject({ + launchState: 'failed_to_start', + spawnStatus: 'error', + runtimeAlive: false, + hardFailure: true, + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + }); + expect(payload.diagnostics).toContain('Runtime is no longer registered'); + }); + + it('prefers stopped runtime liveness over stale spawn liveness in copy diagnostics', () => { + const payload = buildMemberLaunchDiagnosticsPayload({ + teamName: 'signal-ops', + runId: 'bb64da3b-ed5e-4bae-813d-70e26418f9e5', + memberName: 'tom', + spawnEntry: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + runtimeEntry: { + memberName: 'tom', + alive: false, + restartable: true, + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:03.317Z', + }, + }); + + expect(payload).toMatchObject({ + livenessKind: 'not_found', + runtimeAlive: false, + runtimeDiagnostic: 'Runtime is no longer registered', + }); + }); + + it('prefers newer healed snapshots over unsafe live provisioned-but-not-alive diagnostics', () => { + const [payload] = buildTeamMemberLaunchDiagnosticsPayloads({ + teamName: 'signal-ops', + runId: 'run-42', + members: [{ name: 'tom', providerId: 'anthropic' }], + memberSpawnStatuses: { + tom: { + status: 'error', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + memberSpawnSnapshot: { + updatedAt: '2026-05-25T20:14:10.000Z', + statuses: { + tom: { + status: 'online', + launchState: 'confirmed_alive', + agentToolAccepted: true, + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + livenessKind: 'confirmed_bootstrap', + updatedAt: '2026-05-25T20:14:10.000Z', + }, + }, + }, + }); + + expect(payload).toMatchObject({ + memberName: 'tom', + launchState: 'confirmed_alive', + spawnStatus: 'online', + runtimeAlive: true, + hardFailure: false, + }); + }); + it('includes runtime advisory evidence in copy diagnostics', () => { const payload = buildMemberLaunchDiagnosticsPayload({ memberName: 'alice', diff --git a/test/renderer/utils/teamProvisioningPresentation.test.ts b/test/renderer/utils/teamProvisioningPresentation.test.ts index c5a40dae..3a8a5ecc 100644 --- a/test/renderer/utils/teamProvisioningPresentation.test.ts +++ b/test/renderer/utils/teamProvisioningPresentation.test.ts @@ -924,6 +924,127 @@ describe('buildTeamProvisioningPresentation', () => { expect(presentation?.currentStepIndex).toBe(2); }); + it('does not present bootstrap-confirmed provisioned-but-not-alive entries as failed', () => { + const presentation = buildTeamProvisioningPresentation({ + progress: { + runId: 'run-signal-ops', + teamName: 'signal-ops', + state: 'ready', + startedAt: '2026-05-25T20:13:40.000Z', + updatedAt: '2026-05-25T20:14:05.411Z', + message: 'Team provisioned', + messageSeverity: undefined, + pid: 27036, + cliLogsTail: '', + assistantOutput: '', + }, + members: [ + { + name: 'team-lead', + agentType: 'team-lead', + providerId: 'anthropic', + status: 'active', + currentTaskId: null, + taskCount: 0, + lastActiveAt: null, + messageCount: 0, + }, + { + name: 'tom', + providerId: 'anthropic', + laneKind: 'primary', + status: 'active', + currentTaskId: null, + taskCount: 0, + lastActiveAt: null, + messageCount: 0, + }, + ], + memberSpawnStatuses: { + tom: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) \u2014 team provisioned but not alive', + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + memberRuntimeEntries: { + tom: { + memberName: 'tom', + alive: false, + restartable: true, + livenessKind: 'confirmed_bootstrap', + runtimeDiagnostic: + 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:03.317Z', + }, + }, + }); + + expect(presentation?.isFailed).toBe(false); + expect(presentation?.failedSpawnCount).toBe(0); + expect(presentation?.heartbeatConfirmedCount).toBe(1); + expect(presentation?.panelTone).not.toBe('error'); + expect(presentation?.compactTone).not.toBe('error'); + }); + + it('presents unsafe bootstrap-confirmed provisioned-but-not-alive entries as failed', () => { + const presentation = buildTeamProvisioningPresentation({ + progress: { + runId: 'run-signal-ops', + teamName: 'signal-ops', + state: 'ready', + startedAt: '2026-05-25T20:13:40.000Z', + updatedAt: '2026-05-25T20:14:05.411Z', + message: 'Team provisioned', + messageSeverity: undefined, + pid: 27036, + cliLogsTail: '', + assistantOutput: '', + }, + members: [ + { + name: 'tom', + providerId: 'anthropic', + laneKind: 'primary', + status: 'active', + currentTaskId: null, + taskCount: 0, + lastActiveAt: null, + messageCount: 0, + }, + ], + memberSpawnStatuses: { + tom: { + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + livenessKind: 'not_found', + runtimeDiagnostic: 'Runtime is no longer registered', + runtimeDiagnosticSeverity: 'warning', + updatedAt: '2026-05-25T20:14:02.147Z', + }, + }, + }); + + expect(presentation?.isFailed).toBe(false); + expect(presentation?.failedSpawnCount).toBe(1); + expect(presentation?.heartbeatConfirmedCount).toBe(0); + expect(presentation?.successMessageSeverity).toBe('warning'); + expect(presentation?.compactTone).toBe('warning'); + }); + it('does not show core team ready while a primary member is still joining', () => { const presentation = buildTeamProvisioningPresentation({ progress: { diff --git a/test/shared/utils/teamLaunchFailureReason.test.ts b/test/shared/utils/teamLaunchFailureReason.test.ts new file mode 100644 index 00000000..af11a4f9 --- /dev/null +++ b/test/shared/utils/teamLaunchFailureReason.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, it } from 'vitest'; + +import { + hasUnsafeProvisionedButNotAliveRuntimeEvidence, + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext, + isBootstrapConfirmedProvisionedButNotAliveFailure, +} from '@shared/utils/teamLaunchFailureReason'; + +describe('teamLaunchFailureReason', () => { + it('treats runtime process candidates as unsafe provisioned-but-not-alive evidence', () => { + expect( + hasUnsafeProvisionedButNotAliveRuntimeEvidence({ + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + launchState: 'failed_to_start', + livenessKind: 'runtime_process_candidate', + runtimeDiagnostic: + 'OpenCode runtime process detected, but teammate bootstrap is not confirmed', + runtimeDiagnosticSeverity: 'warning', + status: 'error', + }) + ).toBe(true); + }); + + it('treats permission-blocked runtime liveness as unsafe provisioned-but-not-alive evidence', () => { + expect( + hasUnsafeProvisionedButNotAliveRuntimeEvidence({ + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + launchState: 'failed_to_start', + livenessKind: 'permission_blocked', + runtimeDiagnostic: 'runtime is waiting for permission approval', + runtimeDiagnosticSeverity: 'warning', + status: 'error', + }) + ).toBe(true); + }); + + it('keeps process-table-unavailable registered metadata safe for bootstrap healing', () => { + expect( + hasUnsafeProvisionedButNotAliveRuntimeEvidence({ + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + launchState: 'failed_to_start', + livenessKind: 'registered_only', + runtimeDiagnostic: 'runtime pid could not be verified because process table is unavailable', + runtimeDiagnosticSeverity: 'warning', + status: 'error', + }) + ).toBe(false); + }); + + it('treats missing liveness without process-table evidence as unsafe', () => { + expect( + hasUnsafeProvisionedButNotAliveRuntimeEvidence({ + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: 'CLI process exited (code 1) - team provisioned but not alive', + launchState: 'failed_to_start', + status: 'error', + }) + ).toBe(true); + }); + + it('keeps missing liveness safe when process-table evidence is explicit', () => { + expect( + hasUnsafeProvisionedButNotAliveRuntimeEvidence({ + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + launchState: 'failed_to_start', + status: 'error', + }) + ).toBe(false); + }); + + it('uses spawn process-table evidence for registered runtime metadata without diagnostics', () => { + expect( + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext( + { + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + launchState: 'failed_to_start', + status: 'error', + }, + { + livenessKind: 'registered_only', + runtimeDiagnosticSeverity: 'warning', + } + ) + ).toBe(false); + }); + + it('uses spawn process-table evidence for runtime metadata without liveness or diagnostics', () => { + expect( + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext( + { + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + launchState: 'failed_to_start', + status: 'error', + }, + { + runtimeDiagnosticSeverity: 'warning', + } + ) + ).toBe(false); + }); + + it('keeps registered runtime metadata unsafe when runtime diagnostics contradict spawn proof', () => { + expect( + hasUnsafeProvisionedButNotAliveRuntimeEvidenceWithSpawnContext( + { + bootstrapConfirmed: true, + hardFailure: true, + hardFailureReason: + 'CLI process exited (code 1) - team provisioned but not alive; process table unavailable', + launchState: 'failed_to_start', + status: 'error', + }, + { + livenessKind: 'registered_only', + runtimeDiagnostic: 'Runtime heartbeat is not alive', + runtimeDiagnosticSeverity: 'warning', + } + ) + ).toBe(true); + }); + + it('recognizes runtime-diagnostic-only provisioned-but-not-alive failures', () => { + expect( + isBootstrapConfirmedProvisionedButNotAliveFailure({ + bootstrapConfirmed: true, + hardFailure: true, + launchState: 'failed_to_start', + runtimeDiagnostic: 'CLI process exited (code 1) - team provisioned but not alive', + status: 'error', + }) + ).toBe(true); + }); +}); From 5046d80fdf4bc8fcb69e7b06696c87eb9f7ef219 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 12:14:56 +0300 Subject: [PATCH 37/59] test(opencode): update live semantic model results --- .../model-gauntlet-results.json | 24 +++++++++---------- .../model-gauntlet-results.md | 6 ++--- .../report-1779869494489.json | 24 +++++++++++++++++++ 3 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 test-results/opencode-semantic-model-matrix/report-1779869494489.json diff --git a/test-results/opencode-semantic-model-gauntlet/model-gauntlet-results.json b/test-results/opencode-semantic-model-gauntlet/model-gauntlet-results.json index 6e72f050..bb815c19 100644 --- a/test-results/opencode-semantic-model-gauntlet/model-gauntlet-results.json +++ b/test-results/opencode-semantic-model-gauntlet/model-gauntlet-results.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-05-20T15:44:19.975Z", + "generatedAt": "2026-05-27T08:11:47.513Z", "runsPerModel": 1, "qualification": { "minimumAverageScore": 80, @@ -25,8 +25,8 @@ "runtimeTransportFailures": 0, "modelBehaviorFailures": 0, "harnessFailures": 0, - "p50DurationMs": 201184, - "p95DurationMs": 201184, + "p50DurationMs": 129420, + "p95DurationMs": 129420, "stagePassRates": { "launchBootstrap": { "passed": 1, @@ -217,16 +217,16 @@ "outcome": "passed", "failureCategory": "none", "primaryFailure": null, - "durationMs": 201184, + "durationMs": 129420, "hardFailure": false, "stageDurationsMs": { - "setup": 322, - "launchBootstrap": 44102, - "materializeTasks": 40, - "directReply": 20838, - "peerRelayAB": 41022, - "peerRelayBC": 47832, - "concurrentReplies": 29138, + "setup": 168, + "launchBootstrap": 31364, + "materializeTasks": 29, + "directReply": 15080, + "peerRelayAB": 31900, + "peerRelayBC": 29178, + "concurrentReplies": 20867, "hygiene": 1 }, "stageFailures": {}, @@ -253,7 +253,7 @@ "latencyStable": true }, "diagnostics": [ - "runId=85e7ecb6-0767-4606-90d2-c926937b22f5" + "runId=37f103a7-cae5-4d48-b578-56cbabb466d9" ] } ] diff --git a/test-results/opencode-semantic-model-gauntlet/model-gauntlet-results.md b/test-results/opencode-semantic-model-gauntlet/model-gauntlet-results.md index 0c4f989e..51f7194a 100644 --- a/test-results/opencode-semantic-model-gauntlet/model-gauntlet-results.md +++ b/test-results/opencode-semantic-model-gauntlet/model-gauntlet-results.md @@ -1,6 +1,6 @@ # OpenCode Model Gauntlet Results -Generated: 2026-05-20T15:44:19.975Z +Generated: 2026-05-27T08:11:47.513Z Runs per model: 1 Recommended threshold: average >= 80, successful runs >= 1, consistency >= 85, hard failures = 0 @@ -13,7 +13,7 @@ Scoring weights: launchBootstrap=15, directReply=10, peerRelayAB=15, peerRelayBC | Model | Verdict | Confidence | Readiness | Consistency | Score Spread | Behavior Avg | Overall Avg | Counted | Pass Runs | Weakest Stage | Weakest TaskRef | Dominant Failure | Blockers | Provider Infra | Runtime Transport | Model Fails | Protocol Runs | p50 | p95 | | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | -| `opencode/big-pickle` | Recommended | low | 100 | 100 | 0 | 100 | 100 | 1/1 | 1/1 | cleanTranscript 1/1 (100%) | concurrentBob 1/1 (100%) | none | - | 0 | 0 | 0 | 0 | 201184ms | 201184ms | +| `opencode/big-pickle` | Recommended | low | 100 | 100 | 0 | 100 | 100 | 1/1 | 1/1 | cleanTranscript 1/1 (100%) | concurrentBob 1/1 (100%) | none | - | 0 | 0 | 0 | 0 | 129420ms | 129420ms | ## opencode/big-pickle @@ -33,5 +33,5 @@ Protocol totals: badMessages=0, duplicateOrMissingTokens=0, affectedRuns=0. | Run | Outcome | Category | Score | Counted | Duration | Failed Stages | Slowest Stage | TaskRefs | Protocol | Diagnostics | | ---: | --- | --- | ---: | --- | ---: | --- | --- | --- | --- | --- | -| 1 | passed | none | 100 | yes | 201184ms | - | peerRelayBC:47832ms | directReply:ok, peerRelayAB:ok, peerRelayBC:ok, concurrentBob:ok, concurrentTom:ok | - | runId=85e7ecb6-0767-4606-90d2-c926937b22f5 | +| 1 | passed | none | 100 | yes | 129420ms | - | peerRelayAB:31900ms | directReply:ok, peerRelayAB:ok, peerRelayBC:ok, concurrentBob:ok, concurrentTom:ok | - | runId=37f103a7-cae5-4d48-b578-56cbabb466d9 | diff --git a/test-results/opencode-semantic-model-matrix/report-1779869494489.json b/test-results/opencode-semantic-model-matrix/report-1779869494489.json new file mode 100644 index 00000000..1e1f1670 --- /dev/null +++ b/test-results/opencode-semantic-model-matrix/report-1779869494489.json @@ -0,0 +1,24 @@ +{ + "generatedAt": "2026-05-27T08:11:34.489Z", + "models": [ + { + "model": "opencode/big-pickle", + "passed": true, + "score": 100, + "durationMs": 86233, + "stages": { + "launchBootstrap": true, + "directReply": true, + "peerRelay": true, + "taskRefs": true, + "longPrompt": true, + "latencyStable": true + }, + "diagnostics": [ + "runId=5a90cf2a-d00e-4e26-a514-5efecf1914af", + "directDelivery={\"delivered\":true,\"accepted\":true,\"responsePending\":false,\"responseState\":\"responded_visible_message\",\"ledgerStatus\":\"responded\",\"visibleReplyMessageId\":\"d4065728-b244-4e53-a8cf-d33d7de62a6f\",\"visibleReplyCorrelation\":\"relayOfMessageId\",\"diagnostics\":[\"OpenCode app MCP is connected for message delivery.\",\"OpenCode prompt_async accepted after a turn-settled guard; response observation remains delegated to durable app-side ledger reconciliation.\"]}", + "peerDelivery={\"delivered\":true,\"accepted\":true,\"responsePending\":false,\"responseState\":\"responded_visible_message\",\"ledgerStatus\":\"responded\",\"visibleReplyMessageId\":\"37187280-1220-44da-a5d3-a3fdf812cc3a\",\"visibleReplyCorrelation\":\"relayOfMessageId\",\"diagnostics\":[\"OpenCode app MCP is connected for message delivery.\",\"OpenCode prompt_async accepted after a turn-settled guard; response observation remains delegated to durable app-side ledger reconciliation.\"]}" + ] + } + ] +} From e363394c72e475712740dd8cbbe33efc10660302 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 15:28:41 +0300 Subject: [PATCH 38/59] fix(ci): resolve tmp audit failure --- package.json | 1 + pnpm-lock.yaml | 9 +++++---- pnpm-workspace.yaml | 2 ++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index f6f90065..ebbda85d 100644 --- a/package.json +++ b/package.json @@ -429,6 +429,7 @@ "smol-toml": "1.6.1", "srvx": "0.11.13", "tar": "7.5.11", + "tmp": "0.2.6", "undici@7": "7.24.0", "unhead": "2.1.13", "uuid": "^11.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 403e7eda..a635693b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,7 @@ overrides: smol-toml: 1.6.1 srvx: 0.11.13 tar: 7.5.11 + tmp: 0.2.6 undici@7: 7.24.0 unhead: 2.1.13 uuid: ^11.1.1 @@ -10541,8 +10542,8 @@ packages: tmp-promise@3.0.3: resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} - tmp@0.2.5: - resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + tmp@0.2.6: + resolution: {integrity: sha512-5sJPdPjfI5Kx+qbrDesxkglRBxW//g7hCsqspEjwkewGvBMGIKMOTKzLt1hFVJzyadba3lDUN20O9qhvbQUSTA==} engines: {node: '>=14.14'} to-regex-range@5.0.1: @@ -23116,9 +23117,9 @@ snapshots: tmp-promise@3.0.3: dependencies: - tmp: 0.2.5 + tmp: 0.2.6 - tmp@0.2.5: {} + tmp@0.2.6: {} to-regex-range@5.0.1: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ea70d35d..2dea1987 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,6 +4,8 @@ packages: - landing - packages/agent-graph minimumReleaseAge: 4320 +minimumReleaseAgeExclude: + - tmp@0.2.6 strictPeerDependencies: true peerDependencyRules: allowedVersions: From 7cc1a59bbc55c20d221809711e977d8d443bb709 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 18:22:10 +0300 Subject: [PATCH 39/59] fix(team): preserve mixed provider runtime settings --- landing/composables/usePageSeo.ts | 2 +- landing/error.vue | 2 +- landing/nuxt.config.ts | 2 +- landing/product-docs/.vitepress/config.ts | 5 +- landing/public/og-image-agent-teams-v6.png | Bin 0 -> 689240 bytes landing/server/routes/sitemap.xml.ts | 5 +- scripts/dev-with-runtime.mjs | 1 + .../runtime/teamRuntimeSettingsBundle.ts | 1 + .../services/team/TeamProvisioningService.ts | 49 +- .../TeamProvisioningDirectRestart.ts | 2 + .../runtime/teamRuntimeSettingsBundle.test.ts | 20 +- .../team/MixedProviderTeamLaunch.live.test.ts | 64 +++ .../TeamProvisioningDirectRestart.test.ts | 4 + ...ovisioningMemberMcpConfig.safe-e2e.test.ts | 2 + .../team/TeamProvisioningService.test.ts | 2 + .../TeamProvisioningServicePrepare.test.ts | 460 ++++++++++++++++++ .../TeamProvisioningServicePrompts.test.ts | 208 +++++++- 17 files changed, 812 insertions(+), 17 deletions(-) create mode 100644 landing/public/og-image-agent-teams-v6.png diff --git a/landing/composables/usePageSeo.ts b/landing/composables/usePageSeo.ts index 61b0c852..e8ee947c 100644 --- a/landing/composables/usePageSeo.ts +++ b/landing/composables/usePageSeo.ts @@ -39,7 +39,7 @@ export const usePageSeo = (titleKey: string, descriptionKey: string, options: Pa const resolvedImage = computed(() => { if (options.image) return options.image; return { - url: "/og-image-agent-teams-v5.png", + url: "/og-image-agent-teams-v6.png", width: 1200, height: 630, type: "image/png", diff --git a/landing/error.vue b/landing/error.vue index 0afcf215..8fbb7c3c 100644 --- a/landing/error.vue +++ b/landing/error.vue @@ -9,7 +9,7 @@ const props = defineProps<{ const { t } = useI18n(); const config = useRuntimeConfig(); const siteUrl = ((config.public.siteUrl as string) || "https://777genius.github.io/agent-teams-ai").replace(/\/+$/, ""); -const ogImage = `${siteUrl}/og-image-agent-teams-v5.png`; +const ogImage = `${siteUrl}/og-image-agent-teams-v6.png`; const statusCode = computed(() => props.error?.statusCode || 404); const isNotFound = computed(() => statusCode.value === 404); diff --git a/landing/nuxt.config.ts b/landing/nuxt.config.ts index a8bdd199..d6219bed 100644 --- a/landing/nuxt.config.ts +++ b/landing/nuxt.config.ts @@ -17,7 +17,7 @@ const basePrefixedDocsPath = `${baseURL.replace(/\/?$/, "/")}docs`; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const defaultSeoTitle = "Agent Teams - AI Agent Orchestration for Developers"; const defaultSeoDescription = "Free, open-source desktop app for AI agent teams. Start with a free model with no auth, then connect Claude, Codex, or OpenCode when you need more models."; -const defaultSeoImage = `${siteUrl.replace(/\/+$/, "")}/og-image-agent-teams-v5.png`; +const defaultSeoImage = `${siteUrl.replace(/\/+$/, "")}/og-image-agent-teams-v6.png`; export default defineNuxtConfig({ compatibilityDate: "2026-01-19", diff --git a/landing/product-docs/.vitepress/config.ts b/landing/product-docs/.vitepress/config.ts index 47b17ff2..18b48fa5 100644 --- a/landing/product-docs/.vitepress/config.ts +++ b/landing/product-docs/.vitepress/config.ts @@ -32,6 +32,7 @@ const publicBaseUrl = const docsUrl = `${publicBaseUrl}docs/`; const downloadUrl = `${publicBaseUrl}download/`; const ruDownloadUrl = `${publicBaseUrl}ru/download/`; +const ogImageUrl = `${publicBaseUrl}og-image-agent-teams-v6.png`; const landingPublicDir = fileURLToPath(new URL("../../public", import.meta.url)); const rootGuide: DefaultTheme.SidebarItem[] = [ @@ -173,7 +174,7 @@ export default defineConfig({ ["meta", { property: "og:title", content: SITE_TITLE }], ["meta", { property: "og:description", content: SITE_DESCRIPTION }], ["meta", { property: "og:url", content: docsUrl }], - ["meta", { property: "og:image", content: `${publicBaseUrl}og-image.png` }], + ["meta", { property: "og:image", content: ogImageUrl }], ["meta", { property: "og:image:width", content: "1200" }], ["meta", { property: "og:image:height", content: "630" }], ["meta", { property: "og:site_name", content: "Agent Teams" }], @@ -181,7 +182,7 @@ export default defineConfig({ ["meta", { name: "twitter:card", content: "summary_large_image" }], ["meta", { name: "twitter:title", content: SITE_TITLE }], ["meta", { name: "twitter:description", content: SITE_DESCRIPTION }], - ["meta", { name: "twitter:image", content: `${publicBaseUrl}og-image.png` }], + ["meta", { name: "twitter:image", content: ogImageUrl }], [ "script", { type: "application/ld+json" }, diff --git a/landing/public/og-image-agent-teams-v6.png b/landing/public/og-image-agent-teams-v6.png new file mode 100644 index 0000000000000000000000000000000000000000..52e586fc06c8c91201397cec225d24c212a7a104 GIT binary patch literal 689240 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

C6A2&Ars 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 z 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 z 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

|UEl+tY{+jIos)OsOFQ!~Tan4KkS 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,tzKNV2PSYxHQF%6EU`BQ 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 { const config = useRuntimeConfig(); const siteUrl = ((config.public.siteUrl as string) || "https://777genius.github.io/agent-teams-ai").replace(/\/+$/, ""); const toSiteUrl = (path: string) => `${siteUrl}${path === "/" ? "/" : `/${path.replace(/^\/+/, "")}`}`; - const homeImagePaths = ["og-image.png", ...screenshots.map((screenshot) => screenshot.path)]; - const downloadImagePaths = ["og-image.png", "logo-192.png"]; + const ogImagePath = "og-image-agent-teams-v6.png"; + const homeImagePaths = [ogImagePath, ...screenshots.map((screenshot) => screenshot.path)]; + const downloadImagePaths = [ogImagePath, "logo-192.png"]; setHeader(event, "content-type", "application/xml; charset=utf-8"); diff --git a/scripts/dev-with-runtime.mjs b/scripts/dev-with-runtime.mjs index c20f751c..28f14d67 100644 --- a/scripts/dev-with-runtime.mjs +++ b/scripts/dev-with-runtime.mjs @@ -523,6 +523,7 @@ async function main() { const uiEnv = { ...process.env, + UV_THREADPOOL_SIZE: process.env.UV_THREADPOOL_SIZE?.trim() || '16', CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH: resolvedRuntime.binaryPath, }; delete uiEnv.CLAUDE_CLI_PATH; diff --git a/src/main/services/runtime/teamRuntimeSettingsBundle.ts b/src/main/services/runtime/teamRuntimeSettingsBundle.ts index 8a01ba3c..4901f465 100644 --- a/src/main/services/runtime/teamRuntimeSettingsBundle.ts +++ b/src/main/services/runtime/teamRuntimeSettingsBundle.ts @@ -48,6 +48,7 @@ export function splitSettingsJsonArgs(args: string[]): SplitSettingsJsonArgsResu const parsed = parseJsonSettingsObject(value); if (parsed) { settingsFragments.push(parsed); + index += 1; continue; } } diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index f20418c9..f5b199b1 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -2198,6 +2198,24 @@ function buildAnthropicCrossProviderDirectAuthEnvPatch( return envPatch; } +const CODEX_CROSS_PROVIDER_SAFE_ENV_KEYS = [ + 'CLAUDE_CODE_CODEX_BACKEND', + 'CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD', + 'CODEX_CLI_PATH', + 'CODEX_HOME', +] as const; + +function buildCodexCrossProviderSafeEnvPatch(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const envPatch: NodeJS.ProcessEnv = {}; + for (const key of CODEX_CROSS_PROVIDER_SAFE_ENV_KEYS) { + const value = env[key]?.trim(); + if (value) { + envPatch[key] = value; + } + } + return envPatch; +} + interface TeamRuntimeAuthContext { teamName?: string; authMaterialId?: string; @@ -2219,6 +2237,7 @@ interface TeamRuntimeLaunchArgsPlan { runtimeTurnSettledHookArgs: string[]; providerArgs: string[]; extraArgs: string[]; + inheritedProviderArgs: string[]; } type WorkspaceTrustProviderArgsResolver = (input: { @@ -4218,6 +4237,7 @@ export class TeamProvisioningService { launchIdentity?: ProviderModelLaunchIdentity | null; envResolution: ProvisioningEnvResolution; extraArgs?: string[]; + inheritedProviderArgs?: string[]; includeAnthropicHelper: boolean; contextLabel: string; }): Promise { @@ -4228,6 +4248,7 @@ export class TeamProvisioningService { : null; const rawProviderArgs = input.envResolution.providerArgs ?? []; const rawExtraArgs = input.extraArgs ?? []; + const rawInheritedProviderArgs = input.inheritedProviderArgs ?? []; if (!helper && resolvedProviderId !== 'anthropic') { return { @@ -4237,6 +4258,7 @@ export class TeamProvisioningService { await this.buildRuntimeTurnSettledHookSettingsArgs(resolvedProviderId), providerArgs: rawProviderArgs, extraArgs: rawExtraArgs, + inheritedProviderArgs: rawInheritedProviderArgs, }; } @@ -4246,15 +4268,29 @@ export class TeamProvisioningService { ); const splitProviderArgs = splitSettingsJsonArgs(providerArgsWithoutHelper); const splitExtraArgs = splitSettingsJsonArgs(rawExtraArgs); + const splitInheritedArgs = splitSettingsJsonArgs(rawInheritedProviderArgs); + const shouldCoalesceInheritedSettings = splitInheritedArgs.settingsFragments.length > 0; if ( helper && (hasPathBasedSettingsArgs(splitProviderArgs.passthroughArgs) || - hasPathBasedSettingsArgs(splitExtraArgs.passthroughArgs)) + hasPathBasedSettingsArgs(splitExtraArgs.passthroughArgs) || + hasPathBasedSettingsArgs(splitInheritedArgs.passthroughArgs)) ) { throw new Error( `${input.contextLabel}: app-managed Anthropic API-key helper cannot be combined with path-based --settings. Use inline JSON settings or remove the custom --settings path.` ); } + if ( + shouldCoalesceInheritedSettings && + !helper && + (hasPathBasedSettingsArgs(splitProviderArgs.passthroughArgs) || + hasPathBasedSettingsArgs(splitExtraArgs.passthroughArgs) || + hasPathBasedSettingsArgs(splitInheritedArgs.passthroughArgs)) + ) { + throw new Error( + `${input.contextLabel}: mixed-provider launch cannot combine app-managed inherited settings with path-based --settings. Use inline JSON settings or remove the custom --settings path.` + ); + } const settingsBundle = await materializeTeamRuntimeSettingsBundle({ teamName: input.teamName, @@ -4264,6 +4300,7 @@ export class TeamProvisioningService { await this.buildRuntimeTurnSettledHookSettingsObject(resolvedProviderId), ...splitProviderArgs.settingsFragments, ...splitExtraArgs.settingsFragments, + ...splitInheritedArgs.settingsFragments, ], anthropicHelper: helper, settingsDirectory: helper ? null : buildRuntimeSettingsTempDirectory(input.teamName), @@ -4275,6 +4312,7 @@ export class TeamProvisioningService { runtimeTurnSettledHookArgs: [], providerArgs: splitProviderArgs.passthroughArgs, extraArgs: splitExtraArgs.passthroughArgs, + inheritedProviderArgs: splitInheritedArgs.passthroughArgs, }; } @@ -20181,6 +20219,7 @@ export class TeamProvisioningService { launchIdentity, envResolution: { ...provisioningEnv, providerArgs: providerArgsForLaunch }, extraArgs: extraCliArgs, + inheritedProviderArgs: crossProviderMemberArgsForLaunch.args, includeAnthropicHelper: resolvedProviderId === 'anthropic', contextLabel: 'Team create launch', }); @@ -20216,7 +20255,7 @@ export class TeamProvisioningService { ...runtimeArgsPlan.extraArgs, ...runtimeArgsPlan.providerArgs, ...runtimeArgsPlan.settingsArgs, - ...crossProviderMemberArgsForLaunch.args, + ...runtimeArgsPlan.inheritedProviderArgs, ]); const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, { geminiRuntimeAuth, @@ -21509,6 +21548,7 @@ export class TeamProvisioningService { launchIdentity, envResolution: { ...provisioningEnv, providerArgs: providerArgsForLaunch }, extraArgs: extraCliArgs, + inheritedProviderArgs: crossProviderMemberArgsForLaunch.args, includeAnthropicHelper: resolvedProviderId === 'anthropic', contextLabel: 'Team launch', }); @@ -21533,7 +21573,7 @@ export class TeamProvisioningService { // Without this, a codex teammate spawned from an anthropic lead has no way to learn // about the required forced_login_method (chatgpt/api) and fails to start. emitProvisioningCheckpoint(run, 'Resolving cross-provider member launch args'); - launchArgs.push(...crossProviderMemberArgsForLaunch.args); + launchArgs.push(...runtimeArgsPlan.inheritedProviderArgs); const finalLaunchArgs = mergeJsonSettingsArgs(launchArgs); const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, { geminiRuntimeAuth, @@ -35178,6 +35218,9 @@ export class TeamProvisioningService { args.push(...(await this.buildRuntimeTurnSettledHookSettingsArgs(providerId))); const providerArgs = env.providerArgs ?? []; providerArgsByProvider.set(providerId, providerArgs); + if (providerId === 'codex') { + Object.assign(envPatch, buildCodexCrossProviderSafeEnvPatch(env.env)); + } if (env.anthropicApiKeyHelper) { usesAnthropicApiKeyHelper = true; Object.assign(envPatch, env.anthropicApiKeyHelper.envPatch); diff --git a/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts b/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts index 51ab1e90..a8f46aac 100644 --- a/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts +++ b/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts @@ -21,6 +21,8 @@ const DIRECT_TMUX_RESTART_ENV_KEYS = [ 'CLAUDE_CODE_ENTRY_PROVIDER', 'CLAUDE_CODE_GEMINI_BACKEND', 'CLAUDE_CODE_CODEX_BACKEND', + 'CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD', + 'CODEX_CLI_PATH', 'CODEX_HOME', CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_ENV, CLAUDE_TEAM_ANTHROPIC_API_KEY_HELPER_SETTINGS_PATH_ENV, diff --git a/test/main/services/runtime/teamRuntimeSettingsBundle.test.ts b/test/main/services/runtime/teamRuntimeSettingsBundle.test.ts index c1a63348..995dae60 100644 --- a/test/main/services/runtime/teamRuntimeSettingsBundle.test.ts +++ b/test/main/services/runtime/teamRuntimeSettingsBundle.test.ts @@ -1,12 +1,13 @@ // @vitest-environment node +import { + materializeTeamRuntimeSettingsBundle, + splitSettingsJsonArgs, +} from '@main/services/runtime/teamRuntimeSettingsBundle'; import { mkdtemp, readFile, rm } from 'fs/promises'; import { tmpdir } from 'os'; import path from 'path'; - import { afterEach, describe, expect, it } from 'vitest'; -import { materializeTeamRuntimeSettingsBundle } from '@main/services/runtime/teamRuntimeSettingsBundle'; - describe('teamRuntimeSettingsBundle', () => { const tempRoots: string[] = []; @@ -71,4 +72,17 @@ describe('teamRuntimeSettingsBundle', () => { expect(settings.env.ANTHROPIC_AUTH_TOKEN).toBeUndefined(); expect(settings.hooks.Stop).toHaveLength(1); }); + + it('splits equals-style JSON settings without dropping later args', () => { + expect( + splitSettingsJsonArgs([ + '--settings={"codex":{"forced_login_method":"chatgpt"}}', + '--model', + 'gpt-5.5', + ]) + ).toEqual({ + settingsFragments: [{ codex: { forced_login_method: 'chatgpt' } }], + passthroughArgs: ['--model', 'gpt-5.5'], + }); + }); }); diff --git a/test/main/services/team/MixedProviderTeamLaunch.live.test.ts b/test/main/services/team/MixedProviderTeamLaunch.live.test.ts index 76a42071..f1a9a952 100644 --- a/test/main/services/team/MixedProviderTeamLaunch.live.test.ts +++ b/test/main/services/team/MixedProviderTeamLaunch.live.test.ts @@ -303,6 +303,70 @@ liveDescribe('Mixed provider team launch live e2e', () => { ([laneId, lane]) => lane.state === 'active' && laneId === 'secondary:opencode:oscar' ) ).toBe(true); + + await cleanupMixedProviderSmokeTeam(harness, teamName); + + const relaunchProgressEvents: TeamProvisioningProgress[] = []; + await harness.svc.launchTeam( + { + teamName, + cwd: projectPath, + providerId: 'anthropic', + model: anthropicModel, + skipPermissions: true, + clearContext: true, + }, + (progress) => { + relaunchProgressEvents.push(progress); + } + ); + + await waitUntil(async () => { + const last = relaunchProgressEvents.at(-1); + if (last?.state === 'failed') { + throw new Error(formatProgressDump(relaunchProgressEvents)); + } + return last?.state === 'ready'; + }, 360_000); + + await waitUntilWithDiagnostics(async () => { + const status = await harness!.svc.getMemberSpawnStatuses(teamName!); + if (status.teamLaunchState === 'partial_failure') { + throw new Error( + await formatMixedLaunchDiagnostics(harness!, teamName!, relaunchProgressEvents) + ); + } + for (const memberName of ['alice', 'cody', 'oscar'] as const) { + const member = status.statuses[memberName]; + if ( + member?.status !== 'online' || + member.launchState !== 'confirmed_alive' || + member.bootstrapConfirmed !== true + ) { + return false; + } + } + return true; + }, 180_000, () => formatMixedLaunchDiagnostics(harness!, teamName!, relaunchProgressEvents)); + + await waitUntilWithDiagnostics(async () => { + const snapshot = await harness!.svc.getTeamAgentRuntimeSnapshot(teamName!); + return ( + snapshot.members.alice?.providerId === 'anthropic' && + snapshot.members.alice.alive === true && + snapshot.members.cody?.providerId === 'codex' && + snapshot.members.cody.alive === true && + snapshot.members.oscar?.providerId === 'opencode' && + snapshot.members.oscar.alive === true + ); + }, 180_000, () => formatMixedLaunchDiagnostics(harness!, teamName!, relaunchProgressEvents)); + + const relaunchedLaneIndex = await readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName); + expect( + Object.entries(relaunchedLaneIndex.lanes).some( + ([laneId, lane]) => lane.state === 'active' && laneId === 'secondary:opencode:oscar' + ) + ).toBe(true); }, 480_000 ); diff --git a/test/main/services/team/TeamProvisioningDirectRestart.test.ts b/test/main/services/team/TeamProvisioningDirectRestart.test.ts index b1fee3a8..71b5102b 100644 --- a/test/main/services/team/TeamProvisioningDirectRestart.test.ts +++ b/test/main/services/team/TeamProvisioningDirectRestart.test.ts @@ -57,9 +57,11 @@ describe('TeamProvisioningDirectRestart', () => { const assignments = buildDirectTmuxRestartEnvAssignments( { CODEX_HOME: '/tmp/codex home', + CODEX_CLI_PATH: '/opt/codex/bin/codex', CLAUDE_CODE_USE_GEMINI: '1', CLAUDE_CODE_ENTRY_PROVIDER: 'gemini', CLAUDE_CODE_CODEX_BACKEND: 'codex-native', + CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt', }, 'codex' ); @@ -67,9 +69,11 @@ describe('TeamProvisioningDirectRestart', () => { expect(assignments).toContain("CLAUDECODE='1'"); expect(assignments).toContain("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS='1'"); expect(assignments).toContain("CODEX_HOME='/tmp/codex home'"); + expect(assignments).toContain("CODEX_CLI_PATH='/opt/codex/bin/codex'"); expect(assignments).toContain("CLAUDE_CODE_USE_GEMINI=''"); expect(assignments).toContain("CLAUDE_CODE_ENTRY_PROVIDER='codex'"); expect(assignments).toContain("CLAUDE_CODE_CODEX_BACKEND='codex-native'"); + expect(assignments).toContain("CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD='chatgpt'"); expect(assignments).toContain("CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST='1'"); }); diff --git a/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts b/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts index 487871a9..794080a9 100644 --- a/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts +++ b/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts @@ -530,6 +530,7 @@ describe('TeamProvisioningService member MCP config safe e2e', () => { providerArgs: string[]; settingsArgs: string[]; extraArgs: string[]; + inheritedProviderArgs: string[]; }>; } ).buildTeamRuntimeLaunchArgsPlan = vi.fn(async () => ({ @@ -538,6 +539,7 @@ describe('TeamProvisioningService member MCP config safe e2e', () => { providerArgs: [], settingsArgs: [], extraArgs: [], + inheritedProviderArgs: [], })); ( svc as unknown as { updateDirectTmuxRestartMemberConfig: () => Promise } diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index c9f6fc91..b0204629 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -15537,6 +15537,7 @@ describe('TeamProvisioningService', () => { providerArgs: [], settingsArgs: [], extraArgs: [], + inheritedProviderArgs: [], })); (svc as any).materializeDirectProcessNativeBootstrapContext = vi.fn(async () => ({})); (svc as any).updateDirectTmuxRestartMemberConfig = vi.fn(async () => {}); @@ -15635,6 +15636,7 @@ describe('TeamProvisioningService', () => { providerArgs: [], settingsArgs: [], extraArgs: [], + inheritedProviderArgs: [], })); (svc as any).materializeDirectProcessNativeBootstrapContext = vi.fn(async () => ({})); (svc as any).updateDirectTmuxRestartMemberConfig = vi.fn(async () => {}); diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts index 2cfdbd9d..83ae6dc9 100644 --- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts @@ -1,3 +1,4 @@ +import { buildCodexWorkspaceTrustSettingsArgs } from '@features/workspace-trust/core/domain'; import { OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE } from '@shared/utils/openCodeWindowsAccessDenied'; import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection'; import { spawn } from 'child_process'; @@ -555,6 +556,44 @@ describe('TeamProvisioningService prepare/auth behavior', () => { expect(result.args).toContain('--anthropic-safe-passthrough'); }); + it('passes only non-secret Codex runtime env to non-Codex leads for cross-provider teammates', async () => { + const svc = new TeamProvisioningService(); + vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({ + env: { + CLAUDE_CODE_CODEX_BACKEND: 'codex-native', + CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt', + CODEX_CLI_PATH: '/Users/tester/.local/bin/codex', + CODEX_HOME: '/Users/tester/.codex', + OPENAI_API_KEY: 'sk-openai-should-not-leak', + CODEX_API_KEY: 'sk-codex-should-not-leak', + GEMINI_API_KEY: 'gemini-should-not-leak', + ANTHROPIC_API_KEY: 'sk-ant-should-not-leak', + ANTHROPIC_AUTH_TOKEN: 'ant-token-should-not-leak', + }, + authSource: 'codex_runtime', + geminiRuntimeAuth: null, + providerArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'], + }); + + const result = await (svc as any).buildCrossProviderMemberArgs( + 'anthropic', + [{ name: 'jack', providerId: 'codex', model: 'gpt-5.4' }], + { teamRuntimeAuth: { teamName: 'mixed-team', authMaterialId: 'run-1' } } + ); + + expect(result.envPatch).toMatchObject({ + CLAUDE_CODE_CODEX_BACKEND: 'codex-native', + CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt', + CODEX_CLI_PATH: '/Users/tester/.local/bin/codex', + CODEX_HOME: '/Users/tester/.codex', + }); + expect(result.envPatch.OPENAI_API_KEY).toBeUndefined(); + expect(result.envPatch.CODEX_API_KEY).toBeUndefined(); + expect(result.envPatch.GEMINI_API_KEY).toBeUndefined(); + expect(result.envPatch.ANTHROPIC_API_KEY).toBeUndefined(); + expect(result.envPatch.ANTHROPIC_AUTH_TOKEN).toBeUndefined(); + }); + it('passes Anthropic-compatible bearer env to non-Anthropic leads without injecting ANTHROPIC_API_KEY', async () => { const svc = new TeamProvisioningService(); vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({ @@ -3511,6 +3550,427 @@ describe('TeamProvisioningService prepare/auth behavior', () => { expect(settings.hooks.Stop[0].hooks[0].command).toBe('/bin/true # test-hook'); }); + it('coalesces inherited cross-provider JSON settings into the Anthropic runtime settings file', async () => { + const svc = new TeamProvisioningService(); + + const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-codex-inherited-settings-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { providerArgs: [] }, + extraArgs: [], + inheritedProviderArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'], + includeAnthropicHelper: false, + contextLabel: 'Team launch', + }); + + expect(result.settingsArgs[0]).toBe('--settings'); + expect(result.inheritedProviderArgs).toEqual([]); + const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); + expect(settings.codex.forced_login_method).toBe('chatgpt'); + }); + + it('merges provider, extra, and inherited JSON settings in launch precedence order', async () => { + const svc = new TeamProvisioningService(); + + const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-codex-merged-settings-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { + providerArgs: [ + '--settings', + '{"codex":{"forced_login_method":"api","nested":{"provider":true}}}', + '--provider-passthrough', + ], + }, + extraArgs: [ + '--settings={"codex":{"nested":{"extra":true}}}', + '--extra-passthrough', + ], + inheritedProviderArgs: [ + '--settings', + '{"codex":{"forced_login_method":"chatgpt","nested":{"inherited":true}}}', + '--inherited-passthrough', + ], + includeAnthropicHelper: false, + contextLabel: 'Team launch', + }); + + expect(result.providerArgs).toEqual(['--provider-passthrough']); + expect(result.extraArgs).toEqual(['--extra-passthrough']); + expect(result.inheritedProviderArgs).toEqual(['--inherited-passthrough']); + const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); + expect(settings.codex).toMatchObject({ + forced_login_method: 'chatgpt', + nested: { + provider: true, + extra: true, + inherited: true, + }, + }); + }); + + it('coalesces equals-style inherited settings while preserving inherited passthrough args', async () => { + const svc = new TeamProvisioningService(); + + const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-codex-equals-inherited-settings-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { providerArgs: [] }, + extraArgs: [], + inheritedProviderArgs: [ + '--settings={"codex":{"forced_login_method":"chatgpt"}}', + '--safe-inherited-flag', + 'value', + ], + includeAnthropicHelper: false, + contextLabel: 'Team launch', + }); + + expect(result.inheritedProviderArgs).toEqual(['--safe-inherited-flag', 'value']); + const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); + expect(settings.codex.forced_login_method).toBe('chatgpt'); + }); + + it('leaves inherited settings untouched for non-Anthropic lead providers', async () => { + const svc = new TeamProvisioningService(); + const inheritedProviderArgs = ['--settings', '{"anthropic":{"example":true}}']; + + const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'codex-lead-anthropic-inherited-settings-team', + providerId: 'codex', + launchIdentity: null, + envResolution: { providerArgs: [] }, + extraArgs: [], + inheritedProviderArgs, + includeAnthropicHelper: false, + contextLabel: 'Team launch', + }); + + expect(result.settingsArgs).toEqual([]); + expect(result.inheritedProviderArgs).toEqual(inheritedProviderArgs); + }); + + it('coalesces inherited JSON settings into Anthropic helper settings without keeping helper path args', async () => { + const svc = new TeamProvisioningService(); + const helperDir = path.join(tempRoot, 'anthropic-helper'); + const helperSettingsPath = path.join(helperDir, 'settings.json'); + + const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-helper-codex-inherited-settings-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { + providerArgs: ['--settings', helperSettingsPath], + anthropicApiKeyHelper: { + directory: helperDir, + helperPath: path.join(helperDir, 'helper.sh'), + keyPath: path.join(helperDir, 'key'), + settingsPath: helperSettingsPath, + settingsObject: { apiKeyHelper: `'${path.join(helperDir, 'helper.sh')}'` }, + settingsArgs: ['--settings', helperSettingsPath], + envPatch: {}, + }, + }, + extraArgs: [], + inheritedProviderArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'], + includeAnthropicHelper: true, + contextLabel: 'Team launch', + }); + + expect(result.providerArgs).toEqual([]); + expect(result.inheritedProviderArgs).toEqual([]); + expect(result.settingsArgs[0]).toBe('--settings'); + expect(result.settingsArgs[1]).toContain(helperDir); + expect(result.settingsArgs[1]).not.toBe(helperSettingsPath); + const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); + expect(settings.apiKeyHelper).toBe(`'${path.join(helperDir, 'helper.sh')}'`); + expect(settings.codex.forced_login_method).toBe('chatgpt'); + }); + + it('keeps Anthropic helper credentials authoritative over inherited helper-like settings', async () => { + const svc = new TeamProvisioningService(); + const helperDir = path.join(tempRoot, 'anthropic-helper-precedence'); + const helperSettingsPath = path.join(helperDir, 'settings.json'); + const helperPath = path.join(helperDir, 'helper.sh'); + + const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-helper-precedence-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { + providerArgs: ['--settings', helperSettingsPath], + anthropicApiKeyHelper: { + directory: helperDir, + helperPath, + keyPath: path.join(helperDir, 'key'), + settingsPath: helperSettingsPath, + settingsObject: { apiKeyHelper: `'${helperPath}'` }, + settingsArgs: ['--settings', helperSettingsPath], + envPatch: {}, + }, + }, + extraArgs: [], + inheritedProviderArgs: [ + '--settings', + '{"apiKeyHelper":"\\"/tmp/bad-helper.sh\\"","codex":{"forced_login_method":"chatgpt"}}', + ], + includeAnthropicHelper: true, + contextLabel: 'Team launch', + }); + + const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); + expect(settings.apiKeyHelper).toBe(`'${helperPath}'`); + expect(JSON.stringify(settings)).not.toContain('/tmp/bad-helper.sh'); + expect(settings.codex.forced_login_method).toBe('chatgpt'); + }); + + it('coalesces multiple non-primary provider settings without leaking provider secrets into env patch', async () => { + const svc = new TeamProvisioningService(); + vi.spyOn(svc as any, 'buildProvisioningEnv').mockImplementation( + (providerId: unknown) => { + const resolvedProviderId = typeof providerId === 'string' ? providerId : undefined; + if (resolvedProviderId === 'codex') { + return Promise.resolve({ + env: { + CLAUDE_CODE_CODEX_BACKEND: 'codex-native', + CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt', + CODEX_CLI_PATH: '/opt/codex', + CODEX_HOME: '/Users/tester/.codex', + CODEX_API_KEY: 'sk-codex-should-not-leak', + OPENAI_API_KEY: 'sk-openai-should-not-leak', + }, + authSource: 'codex_runtime', + geminiRuntimeAuth: null, + providerArgs: [ + '--settings', + '{"codex":{"forced_login_method":"chatgpt"}}', + '--codex-passthrough', + ], + }); + } + if (resolvedProviderId === 'gemini') { + return Promise.resolve({ + env: { + GEMINI_API_KEY: 'gemini-should-not-leak', + GOOGLE_APPLICATION_CREDENTIALS: '/tmp/gcp-creds.json', + }, + authSource: 'gemini_api_key', + geminiRuntimeAuth: null, + providerArgs: [ + '--settings', + '{"gemini":{"auth_refresh":"gcp"}}', + '--gemini-passthrough', + ], + }); + } + return Promise.resolve({ + env: {}, + authSource: 'none', + geminiRuntimeAuth: null, + providerArgs: [], + }); + } + ); + + const crossProvider = await (svc as any).buildCrossProviderMemberArgs( + 'anthropic', + [ + { name: 'cody', providerId: 'codex', model: 'gpt-5.4' }, + { name: 'gina', providerId: 'gemini', model: 'gemini-2.5-pro' }, + ], + { teamRuntimeAuth: { teamName: 'mixed-team', authMaterialId: 'run-1' } } + ); + const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-codex-gemini-inherited-settings-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { providerArgs: [] }, + extraArgs: [], + inheritedProviderArgs: crossProvider.args, + includeAnthropicHelper: false, + contextLabel: 'Team launch', + }); + + expect(crossProvider.envPatch).toMatchObject({ + CLAUDE_CODE_CODEX_BACKEND: 'codex-native', + CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt', + CODEX_CLI_PATH: '/opt/codex', + CODEX_HOME: '/Users/tester/.codex', + }); + expect(crossProvider.envPatch.CODEX_API_KEY).toBeUndefined(); + expect(crossProvider.envPatch.OPENAI_API_KEY).toBeUndefined(); + expect(crossProvider.envPatch.GEMINI_API_KEY).toBeUndefined(); + expect(crossProvider.envPatch.GOOGLE_APPLICATION_CREDENTIALS).toBeUndefined(); + expect(result.inheritedProviderArgs).toEqual(['--codex-passthrough', '--gemini-passthrough']); + const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); + expect(settings.codex.forced_login_method).toBe('chatgpt'); + expect(settings.gemini.auth_refresh).toBe('gcp'); + }); + + it('coalesces workspace trust patches after inherited cross-provider args are patched', async () => { + const svc = new TeamProvisioningService(); + const trustOverride = 'projects."/repo".trust_level="trusted"'; + const inheritedProviderArgs = (svc as any).applyWorkspaceTrustArgPatches({ + args: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'], + patches: [ + { + id: 'codex-trust', + owner: 'workspace-trust', + targetProvider: 'codex', + targetSurface: 'cross_provider_member_args', + dialect: 'claude-codex-runtime-settings', + args: buildCodexWorkspaceTrustSettingsArgs([trustOverride]), + dedupeKey: 'codex-trust', + sourceWorkspaceIds: ['workspace-1'], + reason: 'Codex native trust is carried through sibling runtime settings.', + }, + ], + targetProvider: 'codex', + targetSurface: 'cross_provider_member_args', + }); + + const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-codex-workspace-trust-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { providerArgs: [] }, + extraArgs: [], + inheritedProviderArgs, + includeAnthropicHelper: false, + contextLabel: 'Team launch', + }); + + expect(result.inheritedProviderArgs).toEqual([]); + const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); + expect(settings.codex).toMatchObject({ + forced_login_method: 'chatgpt', + agent_teams_workspace_trust: { + config_overrides: [trustOverride], + }, + }); + }); + + it('rejects path-based settings when inherited mixed-provider settings must be coalesced', async () => { + const svc = new TeamProvisioningService(); + + await expect( + (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-codex-path-settings-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { providerArgs: [] }, + extraArgs: ['--settings', '/tmp/custom-settings.json'], + inheritedProviderArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'], + includeAnthropicHelper: false, + contextLabel: 'Team launch', + }) + ).rejects.toThrow('mixed-provider launch cannot combine app-managed inherited settings'); + }); + + it('rejects provider path-based settings when inherited mixed-provider settings must be coalesced', async () => { + const svc = new TeamProvisioningService(); + + await expect( + (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-codex-provider-path-settings-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { providerArgs: ['--settings', '/tmp/provider-settings.json'] }, + extraArgs: [], + inheritedProviderArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'], + includeAnthropicHelper: false, + contextLabel: 'Team launch', + }) + ).rejects.toThrow('mixed-provider launch cannot combine app-managed inherited settings'); + }); + + it('rejects inherited path-based settings alongside inherited mixed-provider JSON settings', async () => { + const svc = new TeamProvisioningService(); + + await expect( + (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-codex-inherited-path-settings-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { providerArgs: [] }, + extraArgs: [], + inheritedProviderArgs: [ + '--settings', + '{"codex":{"forced_login_method":"chatgpt"}}', + '--settings', + '/tmp/inherited-custom-settings.json', + ], + includeAnthropicHelper: false, + contextLabel: 'Team launch', + }) + ).rejects.toThrow('mixed-provider launch cannot combine app-managed inherited settings'); + }); + + it('rejects dangling path-based settings when inherited mixed-provider settings must be coalesced', async () => { + const svc = new TeamProvisioningService(); + + await expect( + (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-codex-dangling-settings-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { providerArgs: [] }, + extraArgs: ['--settings'], + inheritedProviderArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'], + includeAnthropicHelper: false, + contextLabel: 'Team launch', + }) + ).rejects.toThrow('mixed-provider launch cannot combine app-managed inherited settings'); + }); + + it('rejects equals-style path settings when inherited mixed-provider settings must be coalesced', async () => { + const svc = new TeamProvisioningService(); + + await expect( + (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-codex-equals-path-settings-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { providerArgs: ['--settings=/tmp/provider-settings.json'] }, + extraArgs: [], + inheritedProviderArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'], + includeAnthropicHelper: false, + contextLabel: 'Team launch', + }) + ).rejects.toThrow('mixed-provider launch cannot combine app-managed inherited settings'); + }); + + it('rejects inherited path-based settings when Anthropic helper settings are app-managed', async () => { + const svc = new TeamProvisioningService(); + + await expect( + (svc as any).buildTeamRuntimeLaunchArgsPlan({ + teamName: 'anthropic-helper-inherited-path-settings-team', + providerId: 'anthropic', + launchIdentity: null, + envResolution: { + providerArgs: [], + anthropicApiKeyHelper: { + directory: '/tmp/anthropic-helper', + helperPath: '/tmp/anthropic-helper/helper.sh', + keyPath: '/tmp/anthropic-helper/key', + settingsPath: '/tmp/anthropic-helper/settings.json', + settingsObject: { apiKeyHelper: "'/tmp/anthropic-helper/helper.sh'" }, + settingsArgs: ['--settings', '/tmp/anthropic-helper/settings.json'], + envPatch: {}, + }, + }, + extraArgs: [], + inheritedProviderArgs: ['--settings', '/tmp/custom-settings.json'], + includeAnthropicHelper: true, + contextLabel: 'Team launch', + }) + ).rejects.toThrow('app-managed Anthropic API-key helper cannot be combined'); + }); + it('adds Codex turn-settled env when Codex is only a secondary member provider', async () => { const svc = new TeamProvisioningService(); svc.setRuntimeTurnSettledEnvironmentProvider(async ({ provider }) => diff --git a/test/main/services/team/TeamProvisioningServicePrompts.test.ts b/test/main/services/team/TeamProvisioningServicePrompts.test.ts index 2a93ec19..8f5d1751 100644 --- a/test/main/services/team/TeamProvisioningServicePrompts.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrompts.test.ts @@ -1,12 +1,10 @@ +import { AGENT_BLOCK_CLOSE, AGENT_BLOCK_OPEN } from '@shared/constants/agentBlocks'; import { EventEmitter } from 'events'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; - import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { AGENT_BLOCK_CLOSE, AGENT_BLOCK_OPEN } from '@shared/constants/agentBlocks'; - const hoisted = vi.hoisted(() => ({ paths: { claudeRoot: '', @@ -109,12 +107,13 @@ vi.mock('@main/utils/pathDecoder', async (importOriginal) => { }; }); +import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver'; +import { TeamRuntimeAdapterRegistry } from '@main/services/team/runtime/TeamRuntimeAdapter'; import { buildAddMemberSpawnMessage, buildRestartMemberSpawnMessage, TeamProvisioningService, } from '@main/services/team/TeamProvisioningService'; -import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver'; import { execCli, spawnCli } from '@main/utils/childProcess'; import { setAppDataBasePath } from '@main/utils/pathDecoder'; @@ -166,6 +165,41 @@ function extractBootstrapSpec(callIndex = 0): { }; } +function readRuntimeSettingsFromLaunchArgs(callIndex = 0): Record { + const args = vi.mocked(spawnCli).mock.calls[callIndex]?.[1] as string[] | undefined; + const settingsFlagIndex = args?.indexOf('--settings') ?? -1; + const settingsValue = settingsFlagIndex >= 0 ? args?.[settingsFlagIndex + 1] : null; + if (!settingsValue) { + throw new Error('Failed to extract runtime settings from spawn args'); + } + if (settingsValue.trim().startsWith('{')) { + return JSON.parse(settingsValue) as Record; + } + return JSON.parse(fs.readFileSync(settingsValue, 'utf8')) as Record; +} + +function registerNoopOpenCodeRuntimeAdapter(svc: TeamProvisioningService): void { + svc.setRuntimeAdapterRegistry( + new TeamRuntimeAdapterRegistry([ + { + providerId: 'opencode', + prepare: vi.fn(async (input: { model?: string }) => ({ + ok: true, + providerId: 'opencode', + modelId: input.model ?? null, + diagnostics: [], + warnings: [], + })), + launch: vi.fn(async () => { + throw new Error('OpenCode side lane launch should not run in this test'); + }), + reconcile: vi.fn(async () => ({ members: {}, warnings: [], diagnostics: [] })), + stop: vi.fn(async () => ({ stopped: true, members: {}, warnings: [], diagnostics: [] })), + } as any, + ]) + ); +} + describe('TeamProvisioningService prompt content (solo mode discipline)', () => { beforeEach(() => { vi.clearAllMocks(); @@ -477,6 +511,65 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () => await svc.cancelProvisioning(runId); }); + it('coalesces codex cross-provider launch overrides into createTeam Anthropic runtime settings', async () => { + vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/claude'); + const { child } = createFakeChild(); + vi.mocked(spawnCli).mockReturnValue(child as any); + + const svc = new TeamProvisioningService(); + registerNoopOpenCodeRuntimeAdapter(svc); + (svc as any).buildProvisioningEnv = vi.fn(async (providerId: string | undefined) => + providerId === 'codex' + ? { + env: { + CLAUDE_CODE_CODEX_BACKEND: 'codex-native', + CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt', + }, + authSource: 'codex_runtime', + geminiRuntimeAuth: null, + providerArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'], + } + : { + env: {}, + authSource: 'none', + geminiRuntimeAuth: null, + providerArgs: [], + } + ); + (svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {}); + (svc as any).startFilesystemMonitor = vi.fn(); + (svc as any).pathExists = vi.fn(async () => false); + + const { runId } = await svc.createTeam( + { + teamName: 'anthropic-codex-create-team', + cwd: process.cwd(), + members: [ + { name: 'alice', role: 'developer', providerId: 'codex' }, + { + name: 'bob', + role: 'reviewer', + providerId: 'opencode', + model: 'minimax-m2.5-free', + }, + ], + providerId: 'anthropic', + }, + () => {} + ); + + const launchArgs = vi.mocked(spawnCli).mock.calls[0]?.[1] as string[]; + expect(launchArgs).not.toContain('{"codex":{"forced_login_method":"chatgpt"}}'); + expect(launchArgs.join(' ')).not.toContain('minimax-m2.5-free'); + expect(extractBootstrapSpec().members).toEqual([ + expect.objectContaining({ name: 'alice', provider: 'codex' }), + ]); + const settings = readRuntimeSettingsFromLaunchArgs(); + expect((settings.codex as Record).forced_login_method).toBe('chatgpt'); + + await svc.cancelProvisioning(runId); + }); + it('blocks Codex xhigh launch effort until runtime exposes reasoning config passthrough', async () => { vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/codex'); vi.mocked(spawnCli).mockReset(); @@ -1065,4 +1158,111 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () => await svc.cancelProvisioning(runId); }); + + it('coalesces codex cross-provider launch overrides into launchTeam Anthropic runtime settings', async () => { + const teamName = 'anthropic-codex-launch-team'; + const teamDir = path.join(tempTeamsBase, teamName); + fs.mkdirSync(teamDir, { recursive: true }); + fs.writeFileSync( + path.join(teamDir, 'config.json'), + JSON.stringify({ + name: teamName, + members: [ + { name: 'team-lead', agentType: 'team-lead', providerId: 'anthropic' }, + { + name: 'alice', + agentType: 'teammate', + role: 'developer', + providerId: 'codex', + model: 'gpt-5.4', + }, + { + name: 'bob', + agentType: 'teammate', + role: 'reviewer', + providerId: 'opencode', + model: 'minimax-m2.5-free', + }, + ], + }), + 'utf8' + ); + + vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/claude'); + const { child } = createFakeChild(); + vi.mocked(spawnCli).mockReturnValue(child as any); + + const svc = new TeamProvisioningService(); + registerNoopOpenCodeRuntimeAdapter(svc); + (svc as any).buildProvisioningEnv = vi.fn(async (providerId: string | undefined) => + providerId === 'codex' + ? { + env: { + CLAUDE_CODE_CODEX_BACKEND: 'codex-native', + CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt', + }, + authSource: 'codex_runtime', + geminiRuntimeAuth: null, + providerArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'], + } + : { + env: {}, + authSource: 'none', + geminiRuntimeAuth: null, + providerArgs: [], + } + ); + (svc as any).resolveProviderDefaultModel = vi.fn(async (providerId: string | undefined) => + providerId === 'codex' ? 'gpt-5.4' : 'opus' + ); + (svc as any).normalizeTeamConfigForLaunch = vi.fn(async () => {}); + (svc as any).updateConfigProjectPath = vi.fn(async () => {}); + (svc as any).restorePrelaunchConfig = vi.fn(async () => {}); + (svc as any).assertConfigLeadOnlyForLaunch = vi.fn(async () => {}); + (svc as any).persistLaunchStateSnapshot = vi.fn(async () => {}); + (svc as any).resolveLaunchExpectedMembers = vi.fn(async () => ({ + members: [ + { + name: 'alice', + role: 'developer', + providerId: 'codex', + model: 'gpt-5.4', + isolation: 'worktree', + }, + { + name: 'bob', + role: 'reviewer', + providerId: 'opencode', + model: 'minimax-m2.5-free', + isolation: 'worktree', + }, + ], + source: 'config-fallback', + warning: undefined, + })); + (svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {}); + (svc as any).pathExists = vi.fn(async () => false); + (svc as any).startFilesystemMonitor = vi.fn(); + + const { runId } = await svc.launchTeam( + { + teamName, + cwd: process.cwd(), + providerId: 'anthropic', + clearContext: true, + } as any, + () => {} + ); + + const launchArgs = vi.mocked(spawnCli).mock.calls[0]?.[1] as string[]; + expect(launchArgs).not.toContain('{"codex":{"forced_login_method":"chatgpt"}}'); + expect(launchArgs.join(' ')).not.toContain('minimax-m2.5-free'); + expect(extractBootstrapSpec().members).toEqual([ + expect.objectContaining({ name: 'alice', provider: 'codex' }), + ]); + const settings = readRuntimeSettingsFromLaunchArgs(); + expect((settings.codex as Record).forced_login_method).toBe('chatgpt'); + + await svc.cancelProvisioning(runId); + }); }); From 0696e7aabed57f6e063b32f9b8890c62fbbc4de3 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 18:27:44 +0300 Subject: [PATCH 40/59] chore(release): use runtime v0.0.48 --- runtime.lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/runtime.lock.json b/runtime.lock.json index 043037c6..eb4173be 100644 --- a/runtime.lock.json +++ b/runtime.lock.json @@ -1,27 +1,27 @@ { - "version": "0.0.47", - "sourceRef": "v0.0.47", + "version": "0.0.48", + "sourceRef": "v0.0.48", "sourceRepository": "777genius/agent_teams_orchestrator", "releaseRepository": "777genius/agent-teams-ai", - "releaseTag": "v2.1.2", + "releaseTag": "v2.2.1", "assets": { "darwin-arm64": { - "file": "agent-teams-runtime-darwin-arm64-v0.0.47.tar.gz", + "file": "agent-teams-runtime-darwin-arm64-v0.0.48.tar.gz", "archiveKind": "tar.gz", "binaryName": "claude-multimodel" }, "darwin-x64": { - "file": "agent-teams-runtime-darwin-x64-v0.0.47.tar.gz", + "file": "agent-teams-runtime-darwin-x64-v0.0.48.tar.gz", "archiveKind": "tar.gz", "binaryName": "claude-multimodel" }, "linux-x64": { - "file": "agent-teams-runtime-linux-x64-v0.0.47.tar.gz", + "file": "agent-teams-runtime-linux-x64-v0.0.48.tar.gz", "archiveKind": "tar.gz", "binaryName": "claude-multimodel" }, "win32-x64": { - "file": "agent-teams-runtime-win32-x64-v0.0.47.zip", + "file": "agent-teams-runtime-win32-x64-v0.0.48.zip", "archiveKind": "zip", "binaryName": "claude-multimodel.exe" } From 5403a2cea914e0d8caf68a787451d9123b026162 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 18:37:46 +0300 Subject: [PATCH 41/59] ci(release): tolerate sentry upload bundles --- scripts/ci/verify-sentry-release.cjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/ci/verify-sentry-release.cjs b/scripts/ci/verify-sentry-release.cjs index 4722b4f1..98952376 100644 --- a/scripts/ci/verify-sentry-release.cjs +++ b/scripts/ci/verify-sentry-release.cjs @@ -98,9 +98,9 @@ function postbuild() { } if (missingDebugIdDirs.length > 0) { - fail( + console.warn( [ - 'Sentry debug IDs were not injected into built JavaScript artifacts', + '[sentry-release] warning: Sentry debug ID comments were not found in built JavaScript artifacts', ...missingDebugIdDirs.map((dir) => ` - ${dir}`), ].join('\n') ); @@ -117,7 +117,7 @@ function postbuild() { } console.log( - `[sentry-release] postbuild ok: ${jsFiles.length} JS artifacts built, debug IDs were injected, and source maps were removed after upload` + `[sentry-release] postbuild ok: ${jsFiles.length} JS artifacts built and source maps were removed after upload` ); } From 77e08af03ff87142b2308a1034de65838e95934d Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 18:56:24 +0300 Subject: [PATCH 42/59] fix(team): propagate managed runtime settings env --- .../services/team/TeamProvisioningService.ts | 25 +++++++++++++++++++ .../TeamProvisioningDirectRestart.ts | 1 + .../TeamProvisioningDirectRestart.test.ts | 2 ++ ...ovisioningMemberMcpConfig.safe-e2e.test.ts | 2 ++ .../team/TeamProvisioningService.test.ts | 2 ++ .../TeamProvisioningServicePrepare.test.ts | 3 +++ .../TeamProvisioningServicePrompts.test.ts | 16 ++++++++++++ 7 files changed, 51 insertions(+) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index f5b199b1..0b2dc3d9 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -1252,6 +1252,7 @@ const STALL_CHECK_INTERVAL_MS = 10_000; const STALL_WARNING_THRESHOLD_MS = 20_000; const APP_TEAM_RUNTIME_DISALLOWED_TOOLS = 'TeamDelete,TodoWrite,TaskCreate,TaskUpdate,mcp__agent-teams__team_launch,mcp__agent-teams__team_stop'; +const CLAUDE_TEAM_RUNTIME_SETTINGS_PATH_ENV = 'CLAUDE_TEAM_RUNTIME_SETTINGS_PATH'; const TEAM_JSON_READ_TIMEOUT_MS = 5_000; const TEAM_CONFIG_MAX_BYTES = 10 * 1024 * 1024; const TEAM_INBOX_MAX_BYTES = 2 * 1024 * 1024; @@ -2216,6 +2217,17 @@ function buildCodexCrossProviderSafeEnvPatch(env: NodeJS.ProcessEnv): NodeJS.Pro return envPatch; } +function applyAppManagedRuntimeSettingsPathEnv( + env: NodeJS.ProcessEnv, + settingsPath: string | null +): void { + if (settingsPath) { + env[CLAUDE_TEAM_RUNTIME_SETTINGS_PATH_ENV] = settingsPath; + } else { + delete env[CLAUDE_TEAM_RUNTIME_SETTINGS_PATH_ENV]; + } +} + interface TeamRuntimeAuthContext { teamName?: string; authMaterialId?: string; @@ -2238,6 +2250,7 @@ interface TeamRuntimeLaunchArgsPlan { providerArgs: string[]; extraArgs: string[]; inheritedProviderArgs: string[]; + appManagedSettingsPath: string | null; } type WorkspaceTrustProviderArgsResolver = (input: { @@ -4259,6 +4272,7 @@ export class TeamProvisioningService { providerArgs: rawProviderArgs, extraArgs: rawExtraArgs, inheritedProviderArgs: rawInheritedProviderArgs, + appManagedSettingsPath: null, }; } @@ -4313,6 +4327,7 @@ export class TeamProvisioningService { providerArgs: splitProviderArgs.passthroughArgs, extraArgs: splitExtraArgs.passthroughArgs, inheritedProviderArgs: splitInheritedArgs.passthroughArgs, + appManagedSettingsPath: settingsBundle?.settingsPath ?? null, }; } @@ -14819,6 +14834,10 @@ export class TeamProvisioningService { includeAnthropicHelper: providerId === 'anthropic', contextLabel: `Direct teammate restart (${input.configuredMember.name})`, }); + applyAppManagedRuntimeSettingsPathEnv( + provisioningEnv.env, + runtimeArgsPlan.appManagedSettingsPath + ); const runtimeArgs = mergeJsonSettingsArgs([ '--agent-id', @@ -15010,6 +15029,10 @@ export class TeamProvisioningService { includeAnthropicHelper: providerId === 'anthropic', contextLabel: `Direct process teammate restart (${input.configuredMember.name})`, }); + applyAppManagedRuntimeSettingsPathEnv( + provisioningEnv.env, + runtimeArgsPlan.appManagedSettingsPath + ); const runtimeArgs = mergeJsonSettingsArgs([ '--teammate-runtime', @@ -20257,6 +20280,7 @@ export class TeamProvisioningService { ...runtimeArgsPlan.settingsArgs, ...runtimeArgsPlan.inheritedProviderArgs, ]); + applyAppManagedRuntimeSettingsPathEnv(shellEnv, runtimeArgsPlan.appManagedSettingsPath); const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, { geminiRuntimeAuth, promptSize, @@ -21575,6 +21599,7 @@ export class TeamProvisioningService { emitProvisioningCheckpoint(run, 'Resolving cross-provider member launch args'); launchArgs.push(...runtimeArgsPlan.inheritedProviderArgs); const finalLaunchArgs = mergeJsonSettingsArgs(launchArgs); + applyAppManagedRuntimeSettingsPathEnv(shellEnv, runtimeArgsPlan.appManagedSettingsPath); const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, { geminiRuntimeAuth, promptSize, diff --git a/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts b/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts index a8f46aac..8f352e39 100644 --- a/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts +++ b/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts @@ -12,6 +12,7 @@ import type { TeamProviderId } from '@shared/types'; const DIRECT_TMUX_RESTART_ENV_KEYS = [ 'CLAUDE_CONFIG_DIR', 'CLAUDE_TEAM_CONTROL_URL', + 'CLAUDE_TEAM_RUNTIME_SETTINGS_PATH', 'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST', 'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_BEDROCK', diff --git a/test/main/services/team/TeamProvisioningDirectRestart.test.ts b/test/main/services/team/TeamProvisioningDirectRestart.test.ts index 71b5102b..70b2d48f 100644 --- a/test/main/services/team/TeamProvisioningDirectRestart.test.ts +++ b/test/main/services/team/TeamProvisioningDirectRestart.test.ts @@ -62,6 +62,7 @@ describe('TeamProvisioningDirectRestart', () => { CLAUDE_CODE_ENTRY_PROVIDER: 'gemini', CLAUDE_CODE_CODEX_BACKEND: 'codex-native', CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt', + CLAUDE_TEAM_RUNTIME_SETTINGS_PATH: '/tmp/runtime-settings.json', }, 'codex' ); @@ -74,6 +75,7 @@ describe('TeamProvisioningDirectRestart', () => { expect(assignments).toContain("CLAUDE_CODE_ENTRY_PROVIDER='codex'"); expect(assignments).toContain("CLAUDE_CODE_CODEX_BACKEND='codex-native'"); expect(assignments).toContain("CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD='chatgpt'"); + expect(assignments).toContain("CLAUDE_TEAM_RUNTIME_SETTINGS_PATH='/tmp/runtime-settings.json'"); expect(assignments).toContain("CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST='1'"); }); diff --git a/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts b/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts index 794080a9..dee558cd 100644 --- a/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts +++ b/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts @@ -531,6 +531,7 @@ describe('TeamProvisioningService member MCP config safe e2e', () => { settingsArgs: string[]; extraArgs: string[]; inheritedProviderArgs: string[]; + appManagedSettingsPath: string | null; }>; } ).buildTeamRuntimeLaunchArgsPlan = vi.fn(async () => ({ @@ -540,6 +541,7 @@ describe('TeamProvisioningService member MCP config safe e2e', () => { settingsArgs: [], extraArgs: [], inheritedProviderArgs: [], + appManagedSettingsPath: null, })); ( svc as unknown as { updateDirectTmuxRestartMemberConfig: () => Promise } diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index b0204629..0494ac95 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -15538,6 +15538,7 @@ describe('TeamProvisioningService', () => { settingsArgs: [], extraArgs: [], inheritedProviderArgs: [], + appManagedSettingsPath: null, })); (svc as any).materializeDirectProcessNativeBootstrapContext = vi.fn(async () => ({})); (svc as any).updateDirectTmuxRestartMemberConfig = vi.fn(async () => {}); @@ -15637,6 +15638,7 @@ describe('TeamProvisioningService', () => { settingsArgs: [], extraArgs: [], inheritedProviderArgs: [], + appManagedSettingsPath: null, })); (svc as any).materializeDirectProcessNativeBootstrapContext = vi.fn(async () => ({})); (svc as any).updateDirectTmuxRestartMemberConfig = vi.fn(async () => {}); diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts index 83ae6dc9..9b7790b3 100644 --- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts @@ -3566,6 +3566,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => { expect(result.settingsArgs[0]).toBe('--settings'); expect(result.inheritedProviderArgs).toEqual([]); + expect(result.appManagedSettingsPath).toBe(result.settingsArgs[1]); const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); expect(settings.codex.forced_login_method).toBe('chatgpt'); }); @@ -3651,6 +3652,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => { expect(result.settingsArgs).toEqual([]); expect(result.inheritedProviderArgs).toEqual(inheritedProviderArgs); + expect(result.appManagedSettingsPath).toBeNull(); }); it('coalesces inherited JSON settings into Anthropic helper settings without keeping helper path args', async () => { @@ -3685,6 +3687,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => { expect(result.settingsArgs[0]).toBe('--settings'); expect(result.settingsArgs[1]).toContain(helperDir); expect(result.settingsArgs[1]).not.toBe(helperSettingsPath); + expect(result.appManagedSettingsPath).toBe(result.settingsArgs[1]); const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); expect(settings.apiKeyHelper).toBe(`'${path.join(helperDir, 'helper.sh')}'`); expect(settings.codex.forced_login_method).toBe('chatgpt'); diff --git a/test/main/services/team/TeamProvisioningServicePrompts.test.ts b/test/main/services/team/TeamProvisioningServicePrompts.test.ts index 8f5d1751..dc3eec6a 100644 --- a/test/main/services/team/TeamProvisioningServicePrompts.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrompts.test.ts @@ -178,6 +178,16 @@ function readRuntimeSettingsFromLaunchArgs(callIndex = 0): Record; } +function readRuntimeSettingsPathFromLaunchArgs(callIndex = 0): string { + const args = vi.mocked(spawnCli).mock.calls[callIndex]?.[1] as string[] | undefined; + const settingsFlagIndex = args?.indexOf('--settings') ?? -1; + const settingsValue = settingsFlagIndex >= 0 ? args?.[settingsFlagIndex + 1] : null; + if (!settingsValue || settingsValue.trim().startsWith('{')) { + throw new Error('Failed to extract runtime settings path from spawn args'); + } + return settingsValue; +} + function registerNoopOpenCodeRuntimeAdapter(svc: TeamProvisioningService): void { svc.setRuntimeAdapterRegistry( new TeamRuntimeAdapterRegistry([ @@ -564,6 +574,9 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () => expect(extractBootstrapSpec().members).toEqual([ expect.objectContaining({ name: 'alice', provider: 'codex' }), ]); + const settingsPath = readRuntimeSettingsPathFromLaunchArgs(); + const launchEnv = vi.mocked(spawnCli).mock.calls[0]?.[2]?.env as NodeJS.ProcessEnv; + expect(launchEnv.CLAUDE_TEAM_RUNTIME_SETTINGS_PATH).toBe(settingsPath); const settings = readRuntimeSettingsFromLaunchArgs(); expect((settings.codex as Record).forced_login_method).toBe('chatgpt'); @@ -1260,6 +1273,9 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () => expect(extractBootstrapSpec().members).toEqual([ expect.objectContaining({ name: 'alice', provider: 'codex' }), ]); + const settingsPath = readRuntimeSettingsPathFromLaunchArgs(); + const launchEnv = vi.mocked(spawnCli).mock.calls[0]?.[2]?.env as NodeJS.ProcessEnv; + expect(launchEnv.CLAUDE_TEAM_RUNTIME_SETTINGS_PATH).toBe(settingsPath); const settings = readRuntimeSettingsFromLaunchArgs(); expect((settings.codex as Record).forced_login_method).toBe('chatgpt'); From 5e12122db755b0a7759b52106fbb4ecd011c845d Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 20:14:25 +0300 Subject: [PATCH 43/59] chore(release): use runtime v0.0.49 --- runtime.lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/runtime.lock.json b/runtime.lock.json index eb4173be..d223cd18 100644 --- a/runtime.lock.json +++ b/runtime.lock.json @@ -1,27 +1,27 @@ { - "version": "0.0.48", - "sourceRef": "v0.0.48", + "version": "0.0.49", + "sourceRef": "v0.0.49", "sourceRepository": "777genius/agent_teams_orchestrator", "releaseRepository": "777genius/agent-teams-ai", - "releaseTag": "v2.2.1", + "releaseTag": "v2.2.2", "assets": { "darwin-arm64": { - "file": "agent-teams-runtime-darwin-arm64-v0.0.48.tar.gz", + "file": "agent-teams-runtime-darwin-arm64-v0.0.49.tar.gz", "archiveKind": "tar.gz", "binaryName": "claude-multimodel" }, "darwin-x64": { - "file": "agent-teams-runtime-darwin-x64-v0.0.48.tar.gz", + "file": "agent-teams-runtime-darwin-x64-v0.0.49.tar.gz", "archiveKind": "tar.gz", "binaryName": "claude-multimodel" }, "linux-x64": { - "file": "agent-teams-runtime-linux-x64-v0.0.48.tar.gz", + "file": "agent-teams-runtime-linux-x64-v0.0.49.tar.gz", "archiveKind": "tar.gz", "binaryName": "claude-multimodel" }, "win32-x64": { - "file": "agent-teams-runtime-win32-x64-v0.0.48.zip", + "file": "agent-teams-runtime-win32-x64-v0.0.49.zip", "archiveKind": "zip", "binaryName": "claude-multimodel.exe" } From 42a3fd683476315858563ee47890fd18ebd0f64a Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 21:53:12 +0300 Subject: [PATCH 44/59] docs(github): refresh issue templates and release badge --- .github/ISSUE_TEMPLATE/bug_report.md | 60 +++++++++++++++++------ .github/ISSUE_TEMPLATE/config.yml | 8 +++ .github/ISSUE_TEMPLATE/feature_request.md | 30 +++++++++--- .github/badges/version.svg | 1 - .github/workflows/release.yml | 31 ------------ README.md | 2 +- 6 files changed, 77 insertions(+), 55 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/badges/version.svg diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5d3171a1..b0cb7b8b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,32 +1,60 @@ --- name: Bug report -about: Create a report to help us improve +about: Report a problem with the Agent Teams desktop app title: "[BUG]" labels: bug assignees: '' --- -**Describe the bug** -A clear and concise description of what the bug is. +**Summary** +A clear description of what went wrong. -**To Reproduce** -Steps to reproduce the behavior: +**Area** +Which part of the app is affected? +- Agent teams / teammate launch +- Team messaging / inboxes +- Tasks / kanban board +- Code review / diffs +- Built-in editor / Git +- Provider runtime (Claude, Codex, OpenCode) +- Settings / authentication +- Installer / updater +- Other: + +**Steps to reproduce** 1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +2. Click on '...' +3. Run / create / send '...' +4. See the problem + +**Frequency** +How often does this happen? [Always / Often / Sometimes / Once] + +**Regression** +Did this work before? If yes, what was the last known good version or commit? + +**Actual behavior** +What happened instead? **Expected behavior** -A clear and concise description of what you expected to happen. +What did you expect to happen? -**Screenshots** -If applicable, add screenshots to help explain your problem. +**Environment** +- OS and version: [e.g. macOS 15.5, Windows 11, Ubuntu 24.04] +- App version or commit hash: +- Install type: [GitHub release / source checkout / other] +- Provider/runtime involved: [Claude / Codex / OpenCode / not sure / not relevant] +- Desktop app mode: Electron -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] +**Logs and diagnostics** +If relevant, include redacted logs or diagnostics. +- Do not paste API keys, access tokens, private repository contents, or other secrets. +- For team launch hangs or missing teammate replies, check the newest artifact pack under `~/.claude/teams//launch-failure-artifacts/latest.json` and include the redacted `manifest.json` summary if you can. +- For UI errors, include the Electron DevTools console error if one is shown. + +**Screenshots or recording** +If applicable, add screenshots or a short recording. **Additional context** -Add any other context about the problem here. +Anything else that might help debug this. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..2154748f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Questions and early ideas + url: https://discord.gg/qtqSZSyuEc + about: Use Discord for support questions, broad ideas, and discussions before opening a large feature request. + - name: Security vulnerability + url: https://github.com/777genius/agent-teams-ai/security/policy + about: Please report undisclosed security issues privately instead of opening a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index a2956b5d..4f22c369 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,20 +1,38 @@ --- name: Feature request -about: Suggest an idea for this project +about: Suggest an improvement for the Agent Teams desktop app title: "[FEAT]" labels: feature request assignees: '' --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +**Summary** +A clear description of the improvement you want. + +**Problem** +What workflow is difficult, slow, confusing, or missing today? + +**Area** +Which part of the app would this affect? +- Agent teams / teammate launch +- Team messaging / inboxes +- Tasks / kanban board +- Code review / diffs +- Built-in editor / Git +- Provider runtime (Claude, Codex, OpenCode) +- Settings / authentication +- Installer / updater +- Other: **Describe the solution you'd like** -A clear and concise description of what you want to happen. +What should the app do? **Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. +What other approaches or workarounds did you try? + +**Success criteria** +How would you know this feature is working well? **Additional context** -Add any other context or screenshots about the feature request here. +Add screenshots, recordings, examples, or related issues if helpful. diff --git a/.github/badges/version.svg b/.github/badges/version.svg deleted file mode 100644 index 274b436e..00000000 --- a/.github/badges/version.svg +++ /dev/null @@ -1 +0,0 @@ -version: v2.1.2versionv2.1.2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba5a2abc..8aed232f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -856,37 +856,6 @@ jobs: TAG="${RELEASE_TAG}" gh release edit "${TAG}" --repo "${GITHUB_REPOSITORY}" --draft=false --latest - - name: Update README version badge - if: ${{ inputs.publish_release }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - DEFAULT_BRANCH="$(gh repo view "${GITHUB_REPOSITORY}" --json defaultBranchRef --jq '.defaultBranchRef.name')" - git fetch origin "${DEFAULT_BRANCH}" - BADGE_WORKTREE="$(mktemp -d)" - git worktree add --detach "${BADGE_WORKTREE}" "origin/${DEFAULT_BRANCH}" - trap 'git worktree remove --force "${BADGE_WORKTREE}" >/dev/null 2>&1 || true' EXIT - - BADGE_LABEL_WIDTH=51 - BADGE_VALUE="${RELEASE_TAG}" - BADGE_VALUE_WIDTH=$(( ${#BADGE_VALUE} * 7 + 10 )) - BADGE_WIDTH=$(( BADGE_LABEL_WIDTH + BADGE_VALUE_WIDTH )) - BADGE_LABEL_X=$(( BADGE_LABEL_WIDTH / 2 )) - BADGE_VALUE_X=$(( BADGE_LABEL_WIDTH + BADGE_VALUE_WIDTH / 2 )) - mkdir -p "${BADGE_WORKTREE}/.github/badges" - cat > "${BADGE_WORKTREE}/.github/badges/version.svg" <version: ${BADGE_VALUE}version${BADGE_VALUE} - EOF - if git -C "${BADGE_WORKTREE}" diff --quiet -- .github/badges/version.svg; then - exit 0 - fi - git -C "${BADGE_WORKTREE}" config user.name "github-actions[bot]" - git -C "${BADGE_WORKTREE}" config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git -C "${BADGE_WORKTREE}" add .github/badges/version.svg - git -C "${BADGE_WORKTREE}" commit -m "docs(readme): update release badge to ${BADGE_VALUE}" - git -C "${BADGE_WORKTREE}" push origin "HEAD:${DEFAULT_BRANCH}" - - name: Keep release as draft if: ${{ github.event_name == 'workflow_dispatch' && !inputs.publish_release }} run: echo "Draft release ${RELEASE_TAG} is ready. It was not published because publish_release=false." diff --git a/README.md b/README.md index 5b53cbc0..85a8d719 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

- Latest Release  + Latest Release  CI Status  Discord

From 877a81439be86b7428733dd6a2289d7603a9da9e Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 21:53:27 +0300 Subject: [PATCH 45/59] fix(member-log-stream): simplify member logs view --- .../adapters/MemberLogStreamSection.tsx | 87 ++++---------- .../renderer/ui/ExecutionLogStreamView.tsx | 6 +- ...emberLogStreamSection.fixture-e2e.test.tsx | 106 ++---------------- 3 files changed, 32 insertions(+), 167 deletions(-) diff --git a/src/features/member-log-stream/renderer/adapters/MemberLogStreamSection.tsx b/src/features/member-log-stream/renderer/adapters/MemberLogStreamSection.tsx index c383ffe9..f44e5e11 100644 --- a/src/features/member-log-stream/renderer/adapters/MemberLogStreamSection.tsx +++ b/src/features/member-log-stream/renderer/adapters/MemberLogStreamSection.tsx @@ -1,15 +1,13 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { useAppTranslation } from '@features/localization/renderer'; -import { api } from '@renderer/api'; import { useStore } from '@renderer/store'; import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice'; import { useMemberLogStream } from '../hooks/useMemberLogStream'; import { ExecutionLogStreamView } from '../ui/ExecutionLogStreamView'; -import { MemberRuntimeProcessLogsPanel } from '../ui/MemberRuntimeProcessLogsPanel'; -import type { MemberLogStreamSegment, MemberRuntimeLogKind } from '../../contracts'; +import type { MemberLogStreamSegment } from '../../contracts'; import type { ResolvedTeamMember } from '@shared/types'; interface MemberLogStreamSectionProps { @@ -19,10 +17,6 @@ interface MemberLogStreamSectionProps { onInitialLoadErrorChange?: (hasError: boolean) => void; } -function describeMemberStream(): string { - return 'Member-scoped transcript and runtime logs rendered with the same execution-log components used in Task Log Stream.'; -} - function getSegmentMetaLabel(segment: MemberLogStreamSegment): string { const details = [segment.source.label]; if (segment.source.laneId) { @@ -45,17 +39,8 @@ export const MemberLogStreamSection = ({ onInitialLoadErrorChange, }: Readonly): React.JSX.Element => { const { t } = useAppTranslation('team'); - const [selectedLogView, setSelectedLogView] = useState<'execution' | 'process'>('execution'); const teamMembers = useStore((s) => selectResolvedMembersForTeamName(s, teamName)); const { stream, loading, error } = useMemberLogStream({ teamName, member, enabled }); - const loadRuntimeLogTail = useCallback( - (input: { - readonly kind: MemberRuntimeLogKind; - readonly maxBytes: number; - readonly forceRefresh?: boolean; - }) => api.memberLogStream.getMemberRuntimeLogTail(teamName, member.name, input), - [member.name, teamName] - ); const hasInitialLoadError = Boolean(error && !stream && !loading); const boundedHistoryNote = useMemo(() => { if (!stream) return null; @@ -71,56 +56,24 @@ export const MemberLogStreamSection = ({ return (
-
- - -
- - {selectedLogView === 'execution' ? ( - - ) : ( - - )} +
); }; diff --git a/src/features/member-log-stream/renderer/ui/ExecutionLogStreamView.tsx b/src/features/member-log-stream/renderer/ui/ExecutionLogStreamView.tsx index 6e165141..b8fb2192 100644 --- a/src/features/member-log-stream/renderer/ui/ExecutionLogStreamView.tsx +++ b/src/features/member-log-stream/renderer/ui/ExecutionLogStreamView.tsx @@ -26,7 +26,7 @@ interface ParticipantVisual { export interface ExecutionLogStreamViewProps { title: string; - description: string; + description?: string; stream: TStream | null; loading: boolean; error: string | null; @@ -312,7 +312,9 @@ export const ExecutionLogStreamView = ({

{title}

-

{description}

+ {description ? ( +

{description}

+ ) : null} ) : null} {boundedHistoryNote ? ( diff --git a/test/renderer/components/team/members/MemberLogStreamSection.fixture-e2e.test.tsx b/test/renderer/components/team/members/MemberLogStreamSection.fixture-e2e.test.tsx index 6029a85a..2cadcfe0 100644 --- a/test/renderer/components/team/members/MemberLogStreamSection.fixture-e2e.test.tsx +++ b/test/renderer/components/team/members/MemberLogStreamSection.fixture-e2e.test.tsx @@ -1,18 +1,17 @@ /* eslint-disable security/detect-non-literal-fs-filename -- Fixture E2E reads a repo fixture and writes temp JSONL. */ -import { readFile, rm, stat, writeFile, mkdtemp } from 'fs/promises'; -import os from 'os'; -import path from 'path'; import React, { act } from 'react'; import { createRoot } from 'react-dom/client'; + +import { mkdtemp, readFile, rm, stat, writeFile } from 'fs/promises'; +import os from 'os'; +import path from 'path'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { GetMemberLogStreamUseCase } from '../../../../../src/features/member-log-stream/core/application/use-cases/GetMemberLogStreamUseCase'; import { type MemberLogStreamRequestOptions, type MemberLogStreamResponse, - type MemberRuntimeLogTailOptions, - type MemberRuntimeLogTailResponse, } from '../../../../../src/features/member-log-stream/contracts'; +import { GetMemberLogStreamUseCase } from '../../../../../src/features/member-log-stream/core/application/use-cases/GetMemberLogStreamUseCase'; import { ClaudeMemberTranscriptStreamSource } from '../../../../../src/features/member-log-stream/main/adapters/output/sources/ClaudeMemberTranscriptStreamSource'; import { OpenCodeMemberRuntimeStreamSource } from '../../../../../src/features/member-log-stream/main/adapters/output/sources/OpenCodeMemberRuntimeStreamSource'; import { BoardTaskExactLogChunkBuilder } from '../../../../../src/main/services/team/taskLogs/exact/BoardTaskExactLogChunkBuilder'; @@ -44,14 +43,6 @@ const apiState = { ) => Promise >(), setMemberLogStreamTracking: vi.fn<(teamName: string, enabled: boolean) => Promise>(), - getMemberRuntimeLogTail: - vi.fn< - ( - teamName: string, - memberName: string, - options: MemberRuntimeLogTailOptions - ) => Promise - >(), onTeamChange: vi.fn<(callback: (event: unknown, data: unknown) => void) => () => void>(), }; @@ -63,9 +54,6 @@ vi.mock('@renderer/api', () => ({ setMemberLogStreamTracking: ( ...args: Parameters ) => apiState.setMemberLogStreamTracking(...args), - getMemberRuntimeLogTail: ( - ...args: Parameters - ) => apiState.getMemberRuntimeLogTail(...args), }, teams: { onTeamChange: (...args: Parameters) => @@ -279,7 +267,6 @@ describe('MemberLogStreamSection real fixture e2e', () => { document.body.innerHTML = ''; apiState.getMemberLogStream.mockReset(); apiState.setMemberLogStreamTracking.mockReset(); - apiState.getMemberRuntimeLogTail.mockReset(); apiState.onTeamChange.mockReset(); vi.unstubAllGlobals(); await Promise.all( @@ -294,13 +281,6 @@ describe('MemberLogStreamSection real fixture e2e', () => { stubMatchMedia(); apiState.onTeamChange.mockImplementation(() => () => undefined); apiState.setMemberLogStreamTracking.mockResolvedValue(undefined); - apiState.getMemberRuntimeLogTail.mockResolvedValue({ - kind: 'stdout', - content: 'process stdout line', - truncated: false, - bytesRead: 19, - missing: false, - }); const { useCase, getOpenCodeTranscript, findRecentMemberLogFileRefsByMember } = await createFixtureUseCase(); @@ -339,8 +319,9 @@ describe('MemberLogStreamSection real fixture e2e', () => { content.includes('Member-wide Claude transcript final note for Jack.') ); - expect(text).toContain('Logs'); - expect(text).toContain('Member-scoped transcript and runtime logs'); + expect(text).not.toContain('Member-scoped transcript and runtime logs'); + expect(text).not.toContain('Execution'); + expect(text).not.toContain('Process'); expect(text).toContain('Claude transcript'); expect(text).toContain('OpenCode runtime'); expect(text).toContain('Calculator behavior'); @@ -396,75 +377,4 @@ describe('MemberLogStreamSection real fixture e2e', () => { expect(apiState.setMemberLogStreamTracking).toHaveBeenCalledWith(TEAM_NAME, true); expect(apiState.setMemberLogStreamTracking).toHaveBeenCalledWith(TEAM_NAME, false); }); - - it('loads bounded process runtime logs after switching the Logs UI to Process', async () => { - vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); - stubMatchMedia(); - apiState.onTeamChange.mockImplementation(() => () => undefined); - apiState.setMemberLogStreamTracking.mockResolvedValue(undefined); - apiState.getMemberLogStream.mockResolvedValue({ - participants: [], - defaultFilter: 'all', - segments: [], - source: 'member_empty', - coverage: [], - warnings: [], - truncated: false, - generatedAt: GENERATED_AT, - metadata: { - scannedTranscriptFileCount: 0, - includedTranscriptFileCount: 0, - droppedSegmentCount: 0, - droppedChunkCount: 0, - droppedMessageCount: 0, - }, - }); - apiState.getMemberRuntimeLogTail.mockResolvedValue({ - kind: 'stdout', - content: 'process stdout line', - truncated: false, - bytesRead: 19, - missing: false, - }); - - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - - await act(async () => { - root.render( - React.createElement( - TooltipProvider, - null, - React.createElement(MemberLogStreamSection, { - teamName: TEAM_NAME, - member: createMember(), - }) - ) - ); - await flushMicrotasks(); - }); - - const processButton = Array.from(host.querySelectorAll('button')).find( - (button) => button.textContent?.trim() === 'Process' - ) as HTMLButtonElement | undefined; - expect(processButton).toBeTruthy(); - - await act(async () => { - processButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })); - await flushAsyncWork(); - }); - - await waitForText(host, (content) => content.includes('process stdout line')); - expect(apiState.getMemberRuntimeLogTail).toHaveBeenCalledWith(TEAM_NAME, MEMBER_NAME, { - kind: 'stdout', - maxBytes: 128 * 1024, - forceRefresh: true, - }); - - await act(async () => { - root.unmount(); - await flushMicrotasks(); - }); - }); }); From 46a525aea1685924ee97c0bd9ca643e46fce2df3 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 21:53:47 +0300 Subject: [PATCH 46/59] fix(cli-status): refresh auth after terminal close --- .../components/dashboard/CliStatusBanner.tsx | 18 --- .../settings/sections/CliStatusSection.tsx | 3 - ...visioningProviderRuntimeSettingsDialog.tsx | 4 - .../cli/CliStatusVisibility.test.ts | 141 +++++++++++++++++- 4 files changed, 140 insertions(+), 26 deletions(-) diff --git a/src/renderer/components/dashboard/CliStatusBanner.tsx b/src/renderer/components/dashboard/CliStatusBanner.tsx index b5ee70a6..63361c61 100644 --- a/src/renderer/components/dashboard/CliStatusBanner.tsx +++ b/src/renderer/components/dashboard/CliStatusBanner.tsx @@ -1759,9 +1759,6 @@ export const CliStatusBanner = (): React.JSX.Element | null => { setProviderTerminal(null); recheckAuthState(); }} - onExit={() => { - recheckAuthState(); - }} autoCloseOnSuccessMs={3000} successMessage={ providerTerminal.action === 'login' @@ -2367,21 +2364,6 @@ export const CliStatusBanner = (): React.JSX.Element | null => { } })(); }} - onExit={() => { - setIsVerifyingAuth(true); - void (async () => { - try { - await invalidateCliStatus(); - if (multimodelEnabled) { - await bootstrapCliStatus({ multimodelEnabled: true }); - } else { - await fetchCliStatus(); - } - } finally { - setIsVerifyingAuth(false); - } - })(); - }} autoCloseOnSuccessMs={4000} successMessage={t('cliStatus.labels.loginComplete')} failureMessage={t('cliStatus.labels.loginFailed')} diff --git a/src/renderer/components/settings/sections/CliStatusSection.tsx b/src/renderer/components/settings/sections/CliStatusSection.tsx index ce5c61b4..3e16b9fb 100644 --- a/src/renderer/components/settings/sections/CliStatusSection.tsx +++ b/src/renderer/components/settings/sections/CliStatusSection.tsx @@ -862,9 +862,6 @@ export const CliStatusSection = (): React.JSX.Element | null => { setProviderTerminal(null); recheckStatus(); }} - onExit={() => { - recheckStatus(); - }} autoCloseOnSuccessMs={3000} successMessage={ providerTerminal.action === 'login' diff --git a/src/renderer/components/team/dialogs/ProvisioningProviderRuntimeSettingsDialog.tsx b/src/renderer/components/team/dialogs/ProvisioningProviderRuntimeSettingsDialog.tsx index 300abf4e..9db050f9 100644 --- a/src/renderer/components/team/dialogs/ProvisioningProviderRuntimeSettingsDialog.tsx +++ b/src/renderer/components/team/dialogs/ProvisioningProviderRuntimeSettingsDialog.tsx @@ -180,10 +180,6 @@ export const ProvisioningProviderRuntimeSettingsDialog = ({ onProviderRuntimeChanged?.(providerTerminal.providerId); refreshRuntimeAfterTerminal(); }} - onExit={() => { - onProviderRuntimeChanged?.(providerTerminal.providerId); - refreshRuntimeAfterTerminal(); - }} autoCloseOnSuccessMs={3000} successMessage={ providerTerminal.action === 'login' ? 'Authentication updated' : 'Provider logged out' diff --git a/test/renderer/components/cli/CliStatusVisibility.test.ts b/test/renderer/components/cli/CliStatusVisibility.test.ts index b6168105..9dcf82cb 100644 --- a/test/renderer/components/cli/CliStatusVisibility.test.ts +++ b/test/renderer/components/cli/CliStatusVisibility.test.ts @@ -70,6 +70,10 @@ let providerRuntimeSettingsDialogProps: { open?: boolean; initialProviderId?: string; } | null = null; +let terminalModalProps: { + onClose?: () => void; + onExit?: (exitCode: number) => void; +} | null = null; const codexAccountHookState = { snapshot: null as CodexAccountSnapshotDto | null, loading: false, @@ -160,7 +164,23 @@ vi.mock('@renderer/components/terminal/TerminalLogPanel', () => ({ })); vi.mock('@renderer/components/terminal/TerminalModal', () => ({ - TerminalModal: () => React.createElement('div', { 'data-testid': 'terminal-modal' }, 'terminal'), + TerminalModal: (props: { onClose?: () => void; onExit?: (exitCode: number) => void }) => { + terminalModalProps = props; + return React.createElement( + 'div', + { 'data-testid': 'terminal-modal' }, + React.createElement( + 'button', + { 'data-testid': 'terminal-exit', onClick: () => props.onExit?.(0) }, + 'exit' + ), + React.createElement( + 'button', + { 'data-testid': 'terminal-close', onClick: () => props.onClose?.() }, + 'close' + ) + ); + }, })); vi.mock('@renderer/store', () => { @@ -345,6 +365,7 @@ describe('CLI status visibility during completed install state', () => { beforeEach(() => { providerRuntimeSettingsDialogProps = null; + terminalModalProps = null; codexAccountHookState.snapshot = null; codexAccountHookState.loading = false; codexAccountHookState.rateLimitsLoading = false; @@ -518,6 +539,50 @@ describe('CLI status visibility during completed install state', () => { }); }); + it('waits until the runtime login modal closes before refreshing auth status', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + storeState.cliStatus = createInstalledCliStatus({ + authLoggedIn: false, + }); + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render(React.createElement(CliStatusBanner)); + await flushLazyImports(); + }); + + const loginButton = Array.from(host.querySelectorAll('button')).find( + (button) => button.textContent?.trim() === 'Login' + ); + + await act(async () => { + loginButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await flushLazyImports(); + }); + + expect(host.querySelector('[data-testid="terminal-modal"]')).not.toBeNull(); + expect(terminalModalProps?.onExit).toBeUndefined(); + + storeState.invalidateCliStatus.mockClear(); + storeState.bootstrapCliStatus.mockClear(); + + await act(async () => { + terminalModalProps?.onClose?.(); + await flushLazyImports(); + }); + + expect(storeState.invalidateCliStatus).toHaveBeenCalledTimes(1); + expect(storeState.bootstrapCliStatus).toHaveBeenCalledTimes(1); + + await act(async () => { + root.unmount(); + await flushLazyImports(); + }); + }); + it('loads the installer terminal log only while installation is active', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.cliInstallerState = 'installing'; @@ -1427,6 +1492,80 @@ describe('CLI status visibility during completed install state', () => { }); }); + it('waits until the provider login modal closes before refreshing provider auth status', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + storeState.cliInstallerState = 'idle'; + storeState.cliStatus = createInstalledCliStatus({ + flavor: 'agent_teams_orchestrator', + authLoggedIn: false, + providers: [ + { + providerId: 'anthropic', + displayName: 'Anthropic', + supported: true, + authenticated: false, + authMethod: null, + verificationState: 'verified', + statusMessage: 'Not connected', + models: [], + canLoginFromUi: true, + capabilities: { + teamLaunch: true, + oneShot: true, + }, + connection: { + supportsOAuth: true, + supportsApiKey: true, + configurableAuthModes: ['auto', 'oauth', 'api_key'], + configuredAuthMode: 'auto', + apiKeyConfigured: false, + apiKeySource: null, + apiKeySourceLabel: null, + }, + backend: null, + }, + ], + }); + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render(React.createElement(CliStatusBanner)); + await flushLazyImports(); + }); + + const connectButton = Array.from(host.querySelectorAll('button')).find((button) => + button.textContent?.includes('Connect Anthropic') + ); + expect(connectButton).not.toBeUndefined(); + + await act(async () => { + connectButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await flushLazyImports(); + }); + + expect(host.querySelector('[data-testid="terminal-modal"]')).not.toBeNull(); + expect(terminalModalProps?.onExit).toBeUndefined(); + + storeState.invalidateCliStatus.mockClear(); + storeState.bootstrapCliStatus.mockClear(); + + await act(async () => { + terminalModalProps?.onClose?.(); + await flushLazyImports(); + }); + + expect(storeState.invalidateCliStatus).toHaveBeenCalledTimes(1); + expect(storeState.bootstrapCliStatus).toHaveBeenCalledTimes(1); + + await act(async () => { + root.unmount(); + await flushLazyImports(); + }); + }); + it('shows subscription limit placeholders while an Anthropic subscription provider is checking', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.cliInstallerState = 'idle'; From 431e3f9a46d7206e7104d3f971f19cda18c12ac5 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 21:54:03 +0300 Subject: [PATCH 47/59] fix(terminal): avoid duplicate strict mode spawns --- .../terminal/EmbeddedTerminal.test.tsx | 172 ++++++++++++++++++ .../components/terminal/EmbeddedTerminal.tsx | 65 ++++--- 2 files changed, 209 insertions(+), 28 deletions(-) create mode 100644 src/renderer/components/terminal/EmbeddedTerminal.test.tsx diff --git a/src/renderer/components/terminal/EmbeddedTerminal.test.tsx b/src/renderer/components/terminal/EmbeddedTerminal.test.tsx new file mode 100644 index 00000000..3abebcd7 --- /dev/null +++ b/src/renderer/components/terminal/EmbeddedTerminal.test.tsx @@ -0,0 +1,172 @@ +import React, { act } from 'react'; +import { createRoot } from 'react-dom/client'; + +import { api } from '@renderer/api'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { EmbeddedTerminal } from './EmbeddedTerminal'; + +vi.mock('@renderer/api', () => ({ + api: { + openExternal: vi.fn(), + terminal: { + spawn: vi.fn(), + write: vi.fn(), + resize: vi.fn(), + kill: vi.fn(), + onData: vi.fn(), + onExit: vi.fn(), + }, + }, +})); + +vi.mock('@xterm/xterm', () => ({ + Terminal: vi.fn().mockImplementation(() => ({ + cols: 80, + rows: 24, + loadAddon: vi.fn(), + open: vi.fn(), + attachCustomKeyEventHandler: vi.fn(), + onData: vi.fn(() => ({ dispose: vi.fn() })), + getSelection: vi.fn(() => ''), + write: vi.fn(), + dispose: vi.fn(), + })), +})); + +vi.mock('@xterm/addon-fit', () => ({ + FitAddon: vi.fn().mockImplementation(() => ({ + fit: vi.fn(), + })), +})); + +vi.mock('@xterm/addon-web-links', () => ({ + WebLinksAddon: vi.fn().mockImplementation(() => ({})), +})); + +let frameCallbacks: Map; +let nextFrameId: number; + +function flushFrames(): void { + const callbacks = Array.from(frameCallbacks.values()); + frameCallbacks.clear(); + callbacks.forEach((callback) => callback(performance.now())); +} + +async function flushReact(): Promise { + await Promise.resolve(); + await Promise.resolve(); +} + +describe('EmbeddedTerminal', () => { + beforeEach(() => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + + frameCallbacks = new Map(); + nextFrameId = 1; + vi.stubGlobal( + 'requestAnimationFrame', + vi.fn((callback: FrameRequestCallback) => { + const id = nextFrameId; + nextFrameId += 1; + frameCallbacks.set(id, callback); + return id; + }) + ); + vi.stubGlobal( + 'cancelAnimationFrame', + vi.fn((id: number) => { + frameCallbacks.delete(id); + }) + ); + vi.stubGlobal( + 'ResizeObserver', + vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + disconnect: vi.fn(), + })) + ); + + vi.mocked(api.terminal.spawn).mockResolvedValue('pty-1'); + vi.mocked(api.terminal.onData).mockReturnValue(() => undefined); + vi.mocked(api.terminal.onExit).mockReturnValue(() => undefined); + }); + + afterEach(() => { + document.body.innerHTML = ''; + vi.clearAllMocks(); + vi.unstubAllGlobals(); + }); + + it('does not spawn twice during React StrictMode effect replay', async () => { + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + + + + ); + await flushReact(); + }); + + expect(api.terminal.spawn).not.toHaveBeenCalled(); + + await act(async () => { + flushFrames(); + await flushReact(); + }); + + expect(api.terminal.spawn).toHaveBeenCalledTimes(1); + expect(api.terminal.spawn).toHaveBeenCalledWith( + expect.objectContaining({ + command: '/bin/runtime', + args: ['auth', 'login'], + }) + ); + + await act(async () => { + root.unmount(); + await flushReact(); + }); + }); + + it('kills a PTY that resolves after the terminal unmounts', async () => { + let resolveSpawn: (id: string) => void = () => {}; + vi.mocked(api.terminal.spawn).mockReturnValue( + new Promise((resolve) => { + resolveSpawn = resolve; + }) + ); + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render(); + await flushReact(); + }); + + await act(async () => { + flushFrames(); + await flushReact(); + }); + + expect(api.terminal.spawn).toHaveBeenCalledTimes(1); + + await act(async () => { + root.unmount(); + await flushReact(); + }); + + await act(async () => { + resolveSpawn('late-pty'); + await flushReact(); + }); + + expect(api.terminal.kill).toHaveBeenCalledWith('late-pty'); + }); +}); diff --git a/src/renderer/components/terminal/EmbeddedTerminal.tsx b/src/renderer/components/terminal/EmbeddedTerminal.tsx index 78acc77d..3e39c938 100644 --- a/src/renderer/components/terminal/EmbeddedTerminal.tsx +++ b/src/renderer/components/terminal/EmbeddedTerminal.tsx @@ -64,8 +64,43 @@ export const EmbeddedTerminal = ({ term.open(container); - // Fit after opening so dimensions are correct - const rafId = requestAnimationFrame(() => fitAddon.fit()); + const spawnTerminal = (): void => { + if (disposed) return; + + const spawnOptions: PtySpawnOptions = { + ...(command ? { command } : {}), + ...(args ? { args } : {}), + ...(cwd ? { cwd } : {}), + ...(env ? { env } : {}), + cols: term.cols, + rows: term.rows, + }; + + api.terminal + .spawn(spawnOptions) + .then((id) => { + if (disposed) { + api.terminal.kill(id); + return; + } + ptyId = id; + // Send actual terminal size after spawn (fitAddon.fit() may have changed cols/rows). + api.terminal.resize(id, term.cols, term.rows); + }) + .catch((err: unknown) => { + if (disposed) return; + term.write( + `\r\n\x1b[31mFailed to start terminal: ${err instanceof Error ? err.message : String(err)}\x1b[0m\r\n` + ); + }); + }; + + // Defer spawning until after the first frame. React StrictMode replays effects + // in development; canceling this RAF prevents duplicate one-shot commands. + const rafId = requestAnimationFrame(() => { + fitAddon.fit(); + spawnTerminal(); + }); // Ctrl+C with selection → copy to clipboard (instead of sending SIGINT) term.attachCustomKeyEventHandler((event) => { @@ -97,32 +132,6 @@ export const EmbeddedTerminal = ({ } }); - // Spawn PTY - const spawnOptions: PtySpawnOptions = { - ...(command ? { command } : {}), - ...(args ? { args } : {}), - ...(cwd ? { cwd } : {}), - ...(env ? { env } : {}), - cols: term.cols, - rows: term.rows, - }; - - api.terminal - .spawn(spawnOptions) - .then((id) => { - if (disposed) return; - ptyId = id; - // Send actual terminal size after spawn (fitAddon.fit() may have - // changed cols/rows via RAF after spawnOptions was constructed) - api.terminal.resize(id, term.cols, term.rows); - }) - .catch((err: unknown) => { - if (disposed) return; - term.write( - `\r\n\x1b[31mFailed to start terminal: ${err instanceof Error ? err.message : String(err)}\x1b[0m\r\n` - ); - }); - // ResizeObserver → fitAddon.fit() → pty.resize() const observer = new ResizeObserver(() => { fitAddon.fit(); From d477d272c52480269dfef53fa2f834c2164ef789 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 21:54:18 +0300 Subject: [PATCH 48/59] fix(textarea): stabilize inline interaction overlays --- .../components/ui/ChipInteractionLayer.tsx | 74 +++++++++++++---- .../ui/MentionInteractionLayer.test.tsx | 82 +++++++++++++++++++ .../components/ui/MentionInteractionLayer.tsx | 43 +++++++++- .../components/ui/MentionableTextarea.tsx | 28 +++---- .../ui/SlashCommandInteractionLayer.tsx | 41 ++++++++-- .../ui/TaskReferenceInteractionLayer.tsx | 46 ++++++++++- .../components/ui/UrlInteractionLayer.tsx | 36 +++++++- 7 files changed, 302 insertions(+), 48 deletions(-) create mode 100644 src/renderer/components/ui/MentionInteractionLayer.test.tsx diff --git a/src/renderer/components/ui/ChipInteractionLayer.tsx b/src/renderer/components/ui/ChipInteractionLayer.tsx index 521c1365..173ed897 100644 --- a/src/renderer/components/ui/ChipInteractionLayer.tsx +++ b/src/renderer/components/ui/ChipInteractionLayer.tsx @@ -171,6 +171,34 @@ interface ChipInteractionLayerProps { onRemove: (chipId: string) => void; } +function areChipsEquivalent(a: InlineChip, b: InlineChip): boolean { + return ( + a.id === b.id && + a.filePath === b.filePath && + a.fileName === b.fileName && + a.fromLine === b.fromLine && + a.toLine === b.toLine && + a.codeText === b.codeText && + a.displayPath === b.displayPath && + a.isFolder === b.isFolder + ); +} + +function areChipPositionsEquivalent(current: ChipPosition[], next: ChipPosition[]): boolean { + if (current.length !== next.length) return false; + + return current.every((position, index) => { + const nextPosition = next[index]; + return ( + position.top === nextPosition.top && + position.left === nextPosition.left && + position.width === nextPosition.width && + position.height === nextPosition.height && + areChipsEquivalent(position.chip, nextPosition.chip) + ); + }); +} + export const ChipInteractionLayer = ({ chips, value, @@ -179,18 +207,25 @@ export const ChipInteractionLayer = ({ onRemove, }: ChipInteractionLayerProps): React.JSX.Element | null => { const [positions, setPositions] = React.useState([]); + const positionsRef = React.useRef([]); const revealFileInEditor = useStore((s) => s.revealFileInEditor); const revealFolderInEditor = useStore((s) => s.revealFolderInEditor); + const commitPositions = React.useCallback((nextPositions: ChipPosition[]) => { + if (areChipPositionsEquivalent(positionsRef.current, nextPositions)) return; + positionsRef.current = nextPositions; + setPositions(nextPositions); + }, []); + React.useLayoutEffect(() => { if (chips.length === 0) { - setPositions([]); + commitPositions([]); return; } const textarea = textareaRef.current; if (!textarea) return; - setPositions(calculateChipPositions(textarea, value, chips)); - }, [chips, value, textareaRef]); + commitPositions(calculateChipPositions(textarea, value, chips)); + }, [chips, commitPositions, value, textareaRef]); if (positions.length === 0) return null; @@ -200,6 +235,14 @@ export const ChipInteractionLayer = ({ {positions.map((pos) => { const isFileChip = pos.chip.fromLine == null; const isFolderChip = pos.chip.isFolder === true; + const openChipTarget = (): void => { + if (isFolderChip) { + revealFolderInEditor(pos.chip.filePath); + } else { + revealFileInEditor(pos.chip.filePath); + } + }; + return ( @@ -211,20 +254,19 @@ export const ChipInteractionLayer = ({ width: pos.width, height: pos.height, }} - onClick={ - isFileChip - ? (e) => { - e.preventDefault(); - e.stopPropagation(); - if (isFolderChip) { - revealFolderInEditor(pos.chip.filePath); - } else { - revealFileInEditor(pos.chip.filePath); - } - } - : undefined - } > + {isFileChip ? ( +

3Ck70+fhept$*=!Se|NQC8udej!2K@mM;=OA< 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