feat: enhance team management with improved session and project path history handling
- Introduced constants for maximum session and project path history limits to optimize memory usage. - Updated `TeamConfigReader` and `TeamProvisioningService` to limit session and project path history to defined maximums. - Enhanced `TeamMembersMetaStore` to handle large meta files more efficiently by checking file size before processing. - Refactored state management in `teamSlice` to include optimized lookups for team summaries by name and session ID.
This commit is contained in:
parent
3b2a0de140
commit
b2e48c6966
6 changed files with 55 additions and 31 deletions
|
|
@ -12,6 +12,8 @@ const logger = createLogger('Service:TeamConfigReader');
|
|||
const TEAM_LIST_CONCURRENCY = process.platform === 'win32' ? 4 : 12;
|
||||
const LARGE_CONFIG_BYTES = 512 * 1024;
|
||||
const CONFIG_HEAD_BYTES = 64 * 1024;
|
||||
const MAX_SESSION_HISTORY_IN_SUMMARY = 2000;
|
||||
const MAX_PROJECT_PATH_HISTORY_IN_SUMMARY = 200;
|
||||
|
||||
async function mapLimit<T, R>(
|
||||
items: readonly T[],
|
||||
|
|
@ -131,10 +133,10 @@ export class TeamConfigReader {
|
|||
? config.leadSessionId
|
||||
: undefined;
|
||||
projectPathHistory = Array.isArray(config.projectPathHistory)
|
||||
? config.projectPathHistory
|
||||
? config.projectPathHistory.slice(-MAX_PROJECT_PATH_HISTORY_IN_SUMMARY)
|
||||
: undefined;
|
||||
sessionHistory = Array.isArray(config.sessionHistory)
|
||||
? config.sessionHistory
|
||||
? config.sessionHistory.slice(-MAX_SESSION_HISTORY_IN_SUMMARY)
|
||||
: undefined;
|
||||
deletedAt = typeof config.deletedAt === 'string' ? config.deletedAt : undefined;
|
||||
}
|
||||
|
|
@ -165,24 +167,6 @@ export class TeamConfigReader {
|
|||
}
|
||||
}
|
||||
|
||||
const removedNames = new Set<string>();
|
||||
try {
|
||||
const metaMembers = await this.membersMetaStore.getMembers(teamName);
|
||||
for (const member of metaMembers) {
|
||||
if (member.removedAt) {
|
||||
removedNames.add(member.name.trim());
|
||||
} else {
|
||||
addMember(member);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.debug(`Failed to read members.meta.json for team: ${teamName}`);
|
||||
}
|
||||
|
||||
for (const name of removedNames) {
|
||||
memberMap.delete(name);
|
||||
}
|
||||
|
||||
const members = Array.from(memberMap.values());
|
||||
const summary: TeamSummary = {
|
||||
teamName,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ interface TeamMembersMetaFile {
|
|||
members: TeamMember[];
|
||||
}
|
||||
|
||||
const MAX_META_FILE_BYTES = 256 * 1024;
|
||||
|
||||
function normalizeMember(member: TeamMember): TeamMember | null {
|
||||
const trimmedName = member.name?.trim();
|
||||
if (!trimmedName) {
|
||||
|
|
@ -35,6 +37,14 @@ export class TeamMembersMetaStore {
|
|||
|
||||
async getMembers(teamName: string): Promise<TeamMember[]> {
|
||||
const metaPath = this.getMetaPath(teamName);
|
||||
try {
|
||||
const stat = await fs.promises.stat(metaPath);
|
||||
if (stat.isFile() && stat.size > MAX_META_FILE_BYTES) {
|
||||
return [];
|
||||
}
|
||||
} catch {
|
||||
// ignore - readFile below will handle ENOENT and throw on other errors
|
||||
}
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await fs.promises.readFile(metaPath, 'utf8');
|
||||
|
|
|
|||
|
|
@ -2620,6 +2620,8 @@ export class TeamProvisioningService {
|
|||
projectPath: string,
|
||||
detectedSessionId: string | null
|
||||
): Promise<void> {
|
||||
const MAX_SESSION_HISTORY = 5000;
|
||||
const MAX_PROJECT_PATH_HISTORY = 500;
|
||||
const configPath = path.join(getTeamsBasePath(), teamName, 'config.json');
|
||||
try {
|
||||
const raw = await fs.promises.readFile(configPath, 'utf8');
|
||||
|
|
@ -2657,7 +2659,11 @@ export class TeamProvisioningService {
|
|||
logger.info(`[${teamName}] Updated leadSessionId: ${newSessionId}`);
|
||||
}
|
||||
|
||||
config.sessionHistory = sessionHistory;
|
||||
if (sessionHistory.length > MAX_SESSION_HISTORY) {
|
||||
config.sessionHistory = sessionHistory.slice(-MAX_SESSION_HISTORY);
|
||||
} else {
|
||||
config.sessionHistory = sessionHistory;
|
||||
}
|
||||
|
||||
// Save current language setting
|
||||
const langCode = ConfigManager.getInstance().getConfig().general.agentLanguage || 'system';
|
||||
|
|
@ -2672,7 +2678,10 @@ export class TeamProvisioningService {
|
|||
)
|
||||
: [];
|
||||
pathHistory.push(projectPath);
|
||||
config.projectPathHistory = pathHistory;
|
||||
config.projectPathHistory =
|
||||
pathHistory.length > MAX_PROJECT_PATH_HISTORY
|
||||
? pathHistory.slice(-MAX_PROJECT_PATH_HISTORY)
|
||||
: pathHistory;
|
||||
}
|
||||
|
||||
await atomicWriteAsync(configPath, JSON.stringify(config, null, 2));
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
|
|||
syncSearchMatchesWithRendered,
|
||||
selectSearchMatch,
|
||||
setTabVisibleAIGroup,
|
||||
teams,
|
||||
openTeamTab,
|
||||
openSessionReport,
|
||||
} = useStore(
|
||||
|
|
@ -79,7 +78,6 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
|
|||
syncSearchMatchesWithRendered: s.syncSearchMatchesWithRendered,
|
||||
selectSearchMatch: s.selectSearchMatch,
|
||||
setTabVisibleAIGroup: s.setTabVisibleAIGroup,
|
||||
teams: s.teams,
|
||||
openTeamTab: s.openTeamTab,
|
||||
openSessionReport: s.openSessionReport,
|
||||
}))
|
||||
|
|
@ -126,12 +124,14 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
|
|||
const thisTab = effectiveTabId ? openTabs.find((t) => t.id === effectiveTabId) : null;
|
||||
const pendingNavigation = thisTab?.pendingNavigation;
|
||||
|
||||
const teamBySessionId = useStore((s) => s.teamBySessionId);
|
||||
|
||||
// Look up whether this session belongs to a team
|
||||
const sessionTeam = useMemo(() => {
|
||||
if (!sessionDetail?.session?.id) return null;
|
||||
const sid = sessionDetail.session.id;
|
||||
return teams.find((t) => t.leadSessionId === sid || t.sessionHistory?.includes(sid)) ?? null;
|
||||
}, [teams, sessionDetail?.session?.id]);
|
||||
const sid = sessionDetail?.session?.id;
|
||||
if (!sid) return null;
|
||||
return teamBySessionId[sid] ?? null;
|
||||
}, [teamBySessionId, sessionDetail?.session?.id]);
|
||||
|
||||
// Compute all accumulated context injections (phase-aware)
|
||||
const { allContextInjections, lastAiGroupTotalTokens } = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export const SidebarTaskItem = ({
|
|||
showTeamName,
|
||||
}: SidebarTaskItemProps): React.JSX.Element => {
|
||||
const openGlobalTaskDetail = useStore((s) => s.openGlobalTaskDetail);
|
||||
const teamMembers = useStore((s) => s.teams.find((t) => t.teamName === task.teamName)?.members);
|
||||
const teamMembers = useStore((s) => s.teamByName[task.teamName]?.members);
|
||||
const unreadCount = useUnreadCommentCount(task.teamName, task.id, task.comments);
|
||||
const cfg =
|
||||
task.kanbanColumn === 'approved'
|
||||
|
|
|
|||
|
|
@ -92,6 +92,10 @@ export interface GlobalTaskDetailState {
|
|||
|
||||
export interface TeamSlice {
|
||||
teams: TeamSummary[];
|
||||
/** O(1) lookup to avoid array scans in render-hot paths */
|
||||
teamByName: Record<string, TeamSummary>;
|
||||
/** O(1) lookup: sessionId -> owning team (lead + history) */
|
||||
teamBySessionId: Record<string, TeamSummary>;
|
||||
teamsLoading: boolean;
|
||||
teamsError: string | null;
|
||||
globalTasks: GlobalTask[];
|
||||
|
|
@ -175,6 +179,8 @@ export interface TeamSlice {
|
|||
|
||||
export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set, get) => ({
|
||||
teams: [],
|
||||
teamByName: {},
|
||||
teamBySessionId: {},
|
||||
teamsLoading: false,
|
||||
teamsError: null,
|
||||
globalTasks: [],
|
||||
|
|
@ -223,7 +229,22 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
}
|
||||
try {
|
||||
const teams = await unwrapIpc('team:list', () => api.teams.list());
|
||||
set({ teams, teamsLoading: false, teamsError: null });
|
||||
const teamByName: Record<string, TeamSummary> = {};
|
||||
const teamBySessionId: Record<string, TeamSummary> = {};
|
||||
for (const team of teams) {
|
||||
teamByName[team.teamName] = team;
|
||||
if (team.leadSessionId) {
|
||||
teamBySessionId[team.leadSessionId] = team;
|
||||
}
|
||||
if (Array.isArray(team.sessionHistory)) {
|
||||
for (const sid of team.sessionHistory) {
|
||||
if (typeof sid === 'string' && sid) {
|
||||
teamBySessionId[sid] = team;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
set({ teams, teamByName, teamBySessionId, teamsLoading: false, teamsError: null });
|
||||
} catch (error) {
|
||||
// On refresh failure, keep existing teams visible
|
||||
set({
|
||||
|
|
@ -313,7 +334,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
|
||||
const state = get();
|
||||
// Use display name from teams list or selected team data if available
|
||||
const teamSummary = state.teams.find((t) => t.teamName === teamName);
|
||||
const teamSummary = state.teamByName[teamName];
|
||||
const displayName = teamSummary?.displayName || state.selectedTeamData?.config.name || teamName;
|
||||
|
||||
const allTabs = state.getAllPaneTabs();
|
||||
|
|
|
|||
Loading…
Reference in a new issue