155 lines
4.4 KiB
TypeScript
155 lines
4.4 KiB
TypeScript
import type {
|
|
MemberWorkSyncEventQueue,
|
|
MemberWorkSyncTriggerReason,
|
|
} from '../../infrastructure/MemberWorkSyncEventQueue';
|
|
import type { TeamChangeEvent, ToolActivityEventPayload } from '@shared/types';
|
|
|
|
interface MemberTurnSettledEventPayload {
|
|
memberName?: string;
|
|
sourceId?: string;
|
|
provider?: string;
|
|
}
|
|
|
|
interface MemberWorkSyncRosterSource {
|
|
loadActiveMemberNames(teamName: string): Promise<string[]>;
|
|
}
|
|
|
|
interface MemberWorkSyncMemberStorageMaterializer {
|
|
materializeMember(teamName: string, memberName: string): Promise<void>;
|
|
}
|
|
|
|
const TEAM_WIDE_REASONS: Partial<Record<TeamChangeEvent['type'], MemberWorkSyncTriggerReason>> = {
|
|
config: 'config_changed',
|
|
task: 'task_changed',
|
|
'task-log-change': 'runtime_activity',
|
|
'log-source-change': 'runtime_activity',
|
|
process: 'runtime_activity',
|
|
'lead-activity': 'runtime_activity',
|
|
};
|
|
|
|
function parseInboxRecipient(detail: string | undefined): string | null {
|
|
if (!detail) {
|
|
return null;
|
|
}
|
|
const match = /^inboxes\/(.+)\.json$/.exec(detail);
|
|
return match?.[1]?.trim() || null;
|
|
}
|
|
|
|
function parseToolActivity(detail: string | undefined): ToolActivityEventPayload | null {
|
|
if (!detail) {
|
|
return null;
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(detail) as ToolActivityEventPayload;
|
|
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function parseMemberTurnSettled(detail: string | undefined): MemberTurnSettledEventPayload | null {
|
|
if (!detail) {
|
|
return null;
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(detail) as MemberTurnSettledEventPayload;
|
|
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export class MemberWorkSyncTeamChangeRouter {
|
|
constructor(
|
|
private readonly rosterSource: MemberWorkSyncRosterSource,
|
|
private readonly queue: MemberWorkSyncEventQueue,
|
|
private readonly materializer?: MemberWorkSyncMemberStorageMaterializer
|
|
) {}
|
|
|
|
async enqueueStartupScan(teamNames: string[]): Promise<void> {
|
|
await Promise.allSettled(
|
|
teamNames.map((teamName) => this.enqueueTeam(teamName, 'startup_scan', 30_000))
|
|
);
|
|
}
|
|
|
|
noteTeamChange(event: TeamChangeEvent): void {
|
|
if (event.type === 'lead-activity' && event.detail === 'offline') {
|
|
this.queue.dropTeam(event.teamName);
|
|
return;
|
|
}
|
|
|
|
if (event.type === 'member-spawn') {
|
|
const memberName = event.detail?.trim();
|
|
if (memberName) {
|
|
this.queue.enqueue({
|
|
teamName: event.teamName,
|
|
memberName,
|
|
triggerReason: 'member_spawned',
|
|
runAfterMs: 30_000,
|
|
});
|
|
} else {
|
|
void this.enqueueTeam(event.teamName, 'member_spawned', 30_000).catch(() => undefined);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (event.type === 'tool-activity') {
|
|
const payload = parseToolActivity(event.detail);
|
|
if (payload?.action === 'finish' && payload.memberName) {
|
|
this.queue.enqueue({
|
|
teamName: event.teamName,
|
|
memberName: payload.memberName,
|
|
triggerReason: 'tool_finished',
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (event.type === 'member-turn-settled') {
|
|
const payload = parseMemberTurnSettled(event.detail);
|
|
const memberName = payload?.memberName?.trim();
|
|
if (memberName) {
|
|
this.queue.enqueue({
|
|
teamName: event.teamName,
|
|
memberName,
|
|
triggerReason: 'turn_settled',
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (event.type === 'inbox' || event.type === 'lead-message') {
|
|
const recipient = parseInboxRecipient(event.detail);
|
|
if (recipient) {
|
|
this.queue.enqueue({
|
|
teamName: event.teamName,
|
|
memberName: recipient,
|
|
triggerReason: 'inbox_changed',
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
const teamWideReason = TEAM_WIDE_REASONS[event.type];
|
|
if (teamWideReason) {
|
|
void this.enqueueTeam(event.teamName, teamWideReason).catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
private async enqueueTeam(
|
|
teamName: string,
|
|
triggerReason: MemberWorkSyncTriggerReason,
|
|
runAfterMs?: number
|
|
): Promise<void> {
|
|
const activeMembers = await this.rosterSource.loadActiveMemberNames(teamName);
|
|
const materializer = this.materializer;
|
|
if (materializer) {
|
|
await Promise.allSettled(
|
|
activeMembers.map((memberName) => materializer.materializeMember(teamName, memberName))
|
|
);
|
|
}
|
|
for (const memberName of activeMembers) {
|
|
this.queue.enqueue({ teamName, memberName, triggerReason, runAfterMs });
|
|
}
|
|
}
|
|
}
|