fix(team): refine change extraction evidence
This commit is contained in:
parent
609f4f8d99
commit
fa829f92c8
2 changed files with 108 additions and 6 deletions
|
|
@ -60,6 +60,8 @@ const OPEN_CODE_MAX_DISCOVERED_LANES = 500;
|
|||
const TEAM_TASK_CHANGE_SUMMARY_BATCH_INSPECT_LIMIT = 1_000;
|
||||
const TEAM_TASK_CHANGE_SUMMARY_BATCH_LIMIT = 200;
|
||||
const TEAM_TASK_CHANGE_SUMMARY_BATCH_CONCURRENCY = 3;
|
||||
const TEAM_TASK_CHANGE_SUMMARY_TASK_TIMEOUT_MS = 15_000;
|
||||
const TEAM_TASK_CHANGE_SUMMARY_BATCH_TIMEOUT_MS = 30_000;
|
||||
|
||||
/** Кеш-запись: данные + mtime файла + время протухания */
|
||||
interface CacheEntry {
|
||||
|
|
@ -88,6 +90,20 @@ interface OpenCodeBackfillAttempt {
|
|||
backfilled: boolean;
|
||||
}
|
||||
|
||||
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
const timeout = new Promise<never>((_resolve, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
|
||||
(timeoutId as ReturnType<typeof setTimeout> & { unref?: () => void }).unref?.();
|
||||
});
|
||||
|
||||
return Promise.race([promise, timeout]).finally(() => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface OpenCodeDeliveryContextTempFile {
|
||||
filePath: string | null;
|
||||
hash: string | null;
|
||||
|
|
@ -361,6 +377,8 @@ export class ChangeExtractorService {
|
|||
changeSet: null,
|
||||
}));
|
||||
let cursor = 0;
|
||||
let timedOut = false;
|
||||
const batchDeadline = Date.now() + TEAM_TASK_CHANGE_SUMMARY_BATCH_TIMEOUT_MS;
|
||||
|
||||
const runNext = async (): Promise<void> => {
|
||||
while (cursor < cappedRequests.length) {
|
||||
|
|
@ -368,14 +386,27 @@ export class ChangeExtractorService {
|
|||
cursor += 1;
|
||||
const request = cappedRequests[index];
|
||||
const taskId = request.taskId.trim();
|
||||
const remainingBatchMs = batchDeadline - Date.now();
|
||||
if (remainingBatchMs <= 0) {
|
||||
timedOut = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const changeSet = await this.getTaskChanges(teamName, taskId, {
|
||||
...request.options,
|
||||
summaryOnly: true,
|
||||
});
|
||||
const changeSet = await withTimeout(
|
||||
this.getTaskChanges(teamName, taskId, {
|
||||
...request.options,
|
||||
summaryOnly: true,
|
||||
}),
|
||||
Math.max(1, Math.min(TEAM_TASK_CHANGE_SUMMARY_TASK_TIMEOUT_MS, remainingBatchMs)),
|
||||
'Task change summary timed out; refresh later or open the task logs.'
|
||||
);
|
||||
items[index] = { taskId, changeSet };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (message.toLowerCase().includes('timed out')) {
|
||||
timedOut = true;
|
||||
}
|
||||
items[index] = {
|
||||
taskId,
|
||||
changeSet: null,
|
||||
|
|
@ -392,12 +423,18 @@ export class ChangeExtractorService {
|
|||
)
|
||||
);
|
||||
|
||||
const responseItems = timedOut
|
||||
? items.filter((item) => item.changeSet !== null || Boolean(item.error))
|
||||
: items;
|
||||
|
||||
return {
|
||||
teamName,
|
||||
items,
|
||||
items: responseItems,
|
||||
computedAt: new Date().toISOString(),
|
||||
truncated:
|
||||
uniqueRequests.length > cappedRequests.length || inspectedRequests < inputRequests.length
|
||||
timedOut ||
|
||||
uniqueRequests.length > cappedRequests.length ||
|
||||
inspectedRequests < inputRequests.length
|
||||
? true
|
||||
: undefined,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -358,6 +358,12 @@ function makeTaskChangeResult(
|
|||
};
|
||||
}
|
||||
|
||||
function pendingTaskChangeResult(): Promise<ReturnType<typeof makeTaskChangeResult>> {
|
||||
return new Promise<ReturnType<typeof makeTaskChangeResult>>(() => {
|
||||
// Keep pending to exercise summary timeout handling.
|
||||
});
|
||||
}
|
||||
|
||||
function createService(params: {
|
||||
logPaths: string[];
|
||||
projectPath?: string;
|
||||
|
|
@ -458,6 +464,65 @@ describe('ChangeExtractorService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('times out slow team task change summaries without blocking faster items', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { service } = createService({ logPaths: [] });
|
||||
const getTaskChanges = vi
|
||||
.spyOn(service, 'getTaskChanges')
|
||||
.mockImplementation((_teamName, taskId) => {
|
||||
if (taskId === 'slow-task') {
|
||||
return pendingTaskChangeResult();
|
||||
}
|
||||
return Promise.resolve(makeTaskChangeResult(taskId, { taskId }));
|
||||
});
|
||||
|
||||
const responsePromise = service.getTeamTaskChangeSummaries(TEAM_NAME, [
|
||||
{ taskId: 'slow-task', options: SUMMARY_OPTIONS },
|
||||
{ taskId: 'fast-task', options: SUMMARY_OPTIONS },
|
||||
]);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(31_000);
|
||||
const response = await responsePromise;
|
||||
|
||||
expect(getTaskChanges).toHaveBeenCalledTimes(2);
|
||||
expect(response.truncated).toBe(true);
|
||||
expect(response.items[0]).toMatchObject({
|
||||
taskId: 'slow-task',
|
||||
changeSet: null,
|
||||
error: expect.stringContaining('timed out'),
|
||||
});
|
||||
expect(response.items[1].changeSet?.taskId).toBe('fast-task');
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('omits unscanned task summary placeholders after the batch deadline', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { service } = createService({ logPaths: [] });
|
||||
vi.spyOn(service, 'getTaskChanges').mockImplementation(() => pendingTaskChangeResult());
|
||||
|
||||
const responsePromise = service.getTeamTaskChangeSummaries(
|
||||
TEAM_NAME,
|
||||
Array.from({ length: 8 }, (_, index) => ({
|
||||
taskId: `slow-task-${index}`,
|
||||
options: SUMMARY_OPTIONS,
|
||||
}))
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(31_000);
|
||||
const response = await responsePromise;
|
||||
|
||||
expect(response.truncated).toBe(true);
|
||||
expect(response.items).toHaveLength(6);
|
||||
expect(response.items.every((item) => item.error?.includes('timed out'))).toBe(true);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('deduplicates team task change summary requests before loading', async () => {
|
||||
const { service } = createService({ logPaths: [] });
|
||||
const getTaskChanges = vi
|
||||
|
|
|
|||
Loading…
Reference in a new issue