From 3e53391a73876d14a25d2ec37221f3e31ddf34d6 Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 23 Apr 2026 01:36:52 +0300 Subject: [PATCH] fix(team): preserve launch error and permission details --- .../bridge/OpenCodeBridgeCommandContract.ts | 1 + .../runtime/OpenCodeTeamRuntimeAdapter.ts | 6 ++ src/renderer/store/slices/teamSlice.ts | 1 + .../team/OpenCodeTeamRuntimeAdapter.test.ts | 2 + test/renderer/store/teamSlice.test.ts | 65 +++++++++++++++++++ 5 files changed, 75 insertions(+) diff --git a/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts b/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts index e8f2bb90..7fb84fe6 100644 --- a/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts +++ b/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts @@ -63,6 +63,7 @@ export interface OpenCodeLaunchTeamCommandBody { export interface OpenCodeTeamMemberLaunchCommandData { sessionId: string; launchState: OpenCodeTeamMemberLaunchBridgeState; + pendingPermissionRequestIds?: string[]; model: string; runtimePid?: number; evidence: Array<{ kind: string; observedAt: string }>; diff --git a/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts b/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts index 2819ce16..e6565935 100644 --- a/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts +++ b/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts @@ -385,6 +385,7 @@ function mapOpenCodeLaunchDataToRuntimeResult( fallbackLaunchState, bridgeMember?.sessionId, bridgeMember?.runtimePid, + bridgeMember?.pendingPermissionRequestIds, bridgeMember != null, [ ...(bridgeMember @@ -426,6 +427,7 @@ function mapBridgeMemberToRuntimeEvidence( launchState: OpenCodeTeamMemberLaunchBridgeState, sessionId: string | undefined, runtimePid: number | undefined, + pendingPermissionRequestIds: string[] | undefined, runtimeMaterialized: boolean, diagnostics: string[] ): TeamRuntimeMemberLaunchEvidence { @@ -448,6 +450,10 @@ function mapBridgeMemberToRuntimeEvidence( bootstrapConfirmed: confirmed, hardFailure: failed, hardFailureReason: failed ? 'OpenCode bridge reported member launch failure' : undefined, + pendingPermissionRequestIds: + pendingPermissionRequestIds && pendingPermissionRequestIds.length > 0 + ? [...new Set(pendingPermissionRequestIds)] + : undefined, sessionId, ...(typeof runtimePid === 'number' && Number.isFinite(runtimePid) && runtimePid > 0 ? { runtimePid } diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 31b03299..b397502d 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -733,6 +733,7 @@ function areMemberSpawnStatusEntriesEqual( left.status === right.status && left.launchState === right.launchState && left.error === right.error && + left.hardFailureReason === right.hardFailureReason && left.livenessSource === right.livenessSource && left.runtimeAlive === right.runtimeAlive && left.runtimeModel === right.runtimeModel && diff --git a/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts b/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts index 0e242992..c2cd7845 100644 --- a/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts +++ b/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts @@ -251,6 +251,7 @@ describe('OpenCodeTeamRuntimeAdapter', () => { alice: { sessionId: 'oc-session-1', launchState: 'permission_blocked', + pendingPermissionRequestIds: ['perm-1', 'perm-1', 'perm-2'], runtimePid: 123, model: 'openai/gpt-5.4-mini', evidence: [ @@ -283,6 +284,7 @@ describe('OpenCodeTeamRuntimeAdapter', () => { alice: { providerId: 'opencode', launchState: 'runtime_pending_permission', + pendingPermissionRequestIds: ['perm-1', 'perm-2'], runtimeAlive: true, agentToolAccepted: true, bootstrapConfirmed: false, diff --git a/test/renderer/store/teamSlice.test.ts b/test/renderer/store/teamSlice.test.ts index 34c4f172..670a44f9 100644 --- a/test/renderer/store/teamSlice.test.ts +++ b/test/renderer/store/teamSlice.test.ts @@ -3463,6 +3463,71 @@ describe('teamSlice actions', () => { expect(store.getState().memberSpawnSnapshotsByTeam['my-team']).toEqual(nextSnapshot); }); + it('rewrites renderer state when only hard failure reason changes', async () => { + const store = createSliceStore(); + const previousSnapshot = createMemberSpawnSnapshot({ + teamLaunchState: 'partial_failure', + summary: { + confirmedCount: 0, + pendingCount: 0, + failedCount: 1, + runtimeAlivePendingCount: 0, + }, + statuses: { + alice: createMemberSpawnStatus({ + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + livenessSource: undefined, + bootstrapConfirmed: false, + hardFailure: true, + hardFailureReason: 'initial failure', + }), + }, + }); + const previousStatuses = previousSnapshot.statuses; + + store.setState({ + currentRuntimeRunIdByTeam: { + 'my-team': 'runtime-run', + }, + memberSpawnStatusesByTeam: { + 'my-team': previousStatuses, + }, + memberSpawnSnapshotsByTeam: { + 'my-team': previousSnapshot, + }, + }); + + const nextSnapshot = createMemberSpawnSnapshot({ + teamLaunchState: 'partial_failure', + summary: { + confirmedCount: 0, + pendingCount: 0, + failedCount: 1, + runtimeAlivePendingCount: 0, + }, + statuses: { + alice: createMemberSpawnStatus({ + status: 'error', + launchState: 'failed_to_start', + runtimeAlive: false, + livenessSource: undefined, + bootstrapConfirmed: false, + hardFailure: true, + hardFailureReason: 'resolved runtime reported missing auth', + }), + }, + }); + hoisted.getMemberSpawnStatuses.mockResolvedValue(nextSnapshot); + + await store.getState().fetchMemberSpawnStatuses('my-team'); + + expect(store.getState().memberSpawnStatusesByTeam['my-team']).not.toBe(previousStatuses); + expect(store.getState().memberSpawnStatusesByTeam['my-team']).toEqual(nextSnapshot.statuses); + expect(store.getState().memberSpawnSnapshotsByTeam['my-team']).toEqual(nextSnapshot); + }); + it('rewrites renderer state when top-level launch summary changes', async () => { const store = createSliceStore(); const previousSnapshot = createMemberSpawnSnapshot({