fix(team): preserve expected members on opencode heartbeat

This commit is contained in:
777genius 2026-04-23 04:02:52 +03:00
parent 98751a2cfc
commit 8332e416a2
2 changed files with 66 additions and 2 deletions

View file

@ -5214,8 +5214,8 @@ export class TeamProvisioningService {
reason: string;
}): Promise<void> {
const previous = await this.launchStateStore.read(input.teamName);
const expectedMembers = previous?.expectedMembers.length
? previous.expectedMembers
const expectedMembers = previous
? this.getPersistedLaunchMemberNames(previous)
: this.readPersistedRuntimeMembers(input.teamName)
.map((member) => (typeof member.name === 'string' ? member.name.trim() : ''))
.filter((name) => name.length > 0 && name !== 'user' && !isLeadMember({ name }));

View file

@ -2620,6 +2620,70 @@ describe('TeamProvisioningService', () => {
});
});
it('preserves richer persisted expectedMembers when OpenCode runtime liveness updates a stale snapshot', async () => {
const svc = new TeamProvisioningService();
const previousSnapshot = {
version: 2 as const,
teamName: 'mixed-team',
updatedAt: '2026-04-22T12:00:00.000Z',
launchPhase: 'active' as const,
expectedMembers: ['bob'],
members: {
alice: {
name: 'alice',
launchState: 'confirmed_alive' as const,
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
lastEvaluatedAt: '2026-04-22T12:00:00.000Z',
},
bob: {
name: 'bob',
providerId: 'opencode' as const,
laneId: 'secondary:opencode:bob',
laneKind: 'secondary' as const,
laneOwnerProviderId: 'opencode' as const,
launchState: 'runtime_pending_bootstrap' as const,
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: false,
hardFailure: false,
lastEvaluatedAt: '2026-04-22T12:00:00.000Z',
},
},
summary: {
confirmedCount: 1,
pendingCount: 1,
failedCount: 0,
runtimeAlivePendingCount: 1,
},
teamLaunchState: 'partial_pending' as const,
};
const write = vi.fn(async () => {});
(svc as any).launchStateStore = {
read: vi.fn(async () => previousSnapshot),
write,
};
await (svc as any).updateOpenCodeRuntimeMemberLiveness({
teamName: 'mixed-team',
runId: 'run-member-spawn-1',
memberName: 'bob',
runtimeSessionId: 'session-bob',
observedAt: '2026-04-22T12:05:00.000Z',
diagnostics: ['native heartbeat'],
reason: 'OpenCode runtime heartbeat accepted',
});
expect(write).toHaveBeenCalledTimes(1);
const writtenSnapshot = (write.mock.calls[0] as unknown as [string, Record<string, unknown>] | undefined)?.[1] as
| { expectedMembers?: string[] }
| undefined;
expect(writtenSnapshot?.expectedMembers).toEqual(['bob', 'alice']);
});
it('accepts secondary OpenCode lane evidence using the lane run id instead of the lead run id', async () => {
const svc = new TeamProvisioningService();