fix: keep supervisor reviews running until completion
Remove elapsed execution-budget pauses from extraction and navigation so long reviews no longer require repeated manual continuation. Remove the pause-duration setting and update activity copy and design documentation. Preserve explicit pause, saved batches, model timeouts and concurrency limits. Validation: recorded 13 focused tests, typecheck and targeted ESLint passed; staged diff check passed. Tests were not rerun for this commit. Release note: 监督回顾现在持续分批处理至完成,不再每隔固定时长自动暂停;仍可主动暂停并从已保存进度继续。
This commit is contained in:
@@ -86,18 +86,31 @@ it('reopens saved batches after failure, resumes exact Unicode offsets and only
|
||||
expect(f.summarize).toHaveBeenCalledTimes(calls)
|
||||
})
|
||||
|
||||
it('soft budget pauses without checkpointing and freezes configuration across continuation', async () => {
|
||||
it('continues extraction and navigation beyond the saved execution budget', async () => {
|
||||
const f = await fixture([['x'.repeat(2500)]], { concurrency: 1, executionSeconds: 30 })
|
||||
const clock = vi.spyOn(Date, 'now').mockReturnValue(time)
|
||||
f.summarize.mockImplementationOnce(async () => { clock.mockReturnValue(time + 31000); return empty })
|
||||
const result = await f.service().run(request)
|
||||
expect(result.status).toBe('completed')
|
||||
expect(result.coverage).toMatchObject({ batches: 3, remainingSources: 0, complete: true })
|
||||
expect(f.summarize).toHaveBeenCalledTimes(5)
|
||||
expect(f.db.supervisionReviewStore().batches(result.runId!, 100).flatMap(row => row.evidence).map(row => row.content.length)).toEqual([1000, 1000, 500])
|
||||
clock.mockRestore()
|
||||
})
|
||||
|
||||
it('still pauses on user request and resumes saved batches without changing batch settings', async () => {
|
||||
const f = await fixture([['x'.repeat(2500)]], { concurrency: 1 })
|
||||
const service = f.service()
|
||||
f.summarize.mockImplementationOnce(async () => empty).mockImplementationOnce(async () => {
|
||||
service.pause(f.db.listSupervisionActivity()[0]!.id)
|
||||
return empty
|
||||
})
|
||||
const result = await service.run(request)
|
||||
expect(result.status).toBe('paused')
|
||||
expect(result.coverage).toMatchObject({ batches: 1, remainingSources: 1, complete: false })
|
||||
f.config.batchCharacters = 16000
|
||||
const resumed = await f.service().resume(result.runId!)
|
||||
expect(resumed.status).toBe('completed')
|
||||
expect((await f.service().resume(result.runId!)).status).toBe('completed')
|
||||
expect(f.db.supervisionReviewStore().batches(result.runId!, 100).flatMap(row => row.evidence).map(row => row.content.length)).toEqual([1000, 1000, 500])
|
||||
clock.mockRestore()
|
||||
})
|
||||
|
||||
it('rejects changed source versions and rolls back offsets when batch persistence fails', async () => {
|
||||
|
||||
@@ -264,10 +264,9 @@ export class SupervisorService {
|
||||
this.controllers.set(runId, controller)
|
||||
const activity = { inFlight: 0 }
|
||||
this.activity.set(runId, activity)
|
||||
const deadline = Date.now() + config.executionSeconds * 1000
|
||||
const empty: SupervisionSummaryOutput = { summary: 'No new evidence', changeDigest: '', openItems: [], events: [], entities: [], entityChanges: [], relations: [] }
|
||||
const paused = () => {
|
||||
db.pause(runId, controller.signal.aborted ? 'Review paused by user' : 'Execution budget reached; continue to review remaining sources')
|
||||
db.pause(runId, 'Review paused by user')
|
||||
return { runId, request, evidence: [], output: empty, status: 'paused' as const, coverage: db.progress(runId) }
|
||||
}
|
||||
try {
|
||||
@@ -300,7 +299,6 @@ export class SupervisorService {
|
||||
if (controller.signal.aborted) return paused()
|
||||
const groups = db.groups(runId, config.concurrency)
|
||||
if (!groups.length) break
|
||||
if (Date.now() >= deadline) return paused()
|
||||
const results = await Promise.allSettled(groups.map(async group => {
|
||||
controller.signal.throwIfAborted()
|
||||
const evidence = db.chunk(runId, group.projectId, group.conversationId, config)
|
||||
@@ -324,7 +322,6 @@ export class SupervisorService {
|
||||
const id = createHash('sha256').update(JSON.stringify([left.id, right.id])).digest('hex')
|
||||
const cached = db.navigation(runId, id)
|
||||
if (cached) return { id, output: cached }
|
||||
if (Date.now() >= deadline) throw new Error('REVIEW_SOFT_BUDGET')
|
||||
const evidence = [left, right].map(card => ({ id: card.id, sourceType: 'note' as const, sourceId: card.id,
|
||||
title: 'Retained review navigation', content: card.output.summary, occurredAt: request.timeRange.to }))
|
||||
const output = await summarize(evidence, true)
|
||||
@@ -378,7 +375,7 @@ export class SupervisorService {
|
||||
await this.store.save(result)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted || (error instanceof Error && error.message === 'REVIEW_SOFT_BUDGET')) return paused()
|
||||
if (controller.signal.aborted) return paused()
|
||||
this.store.fail?.(runId, error instanceof Error ? error.message : 'Review failed')
|
||||
throw error
|
||||
} finally { this.controllers.delete(runId); this.activity.delete(runId) }
|
||||
|
||||
@@ -11,6 +11,8 @@ it.each(['zh-CN', 'en-US'] as const)('shows actionable supervisor help without d
|
||||
render(<SupervisionReviewSettings />)
|
||||
const copy = i18nResources[locale].heartbeat
|
||||
expect(screen.getByText(copy.reviewSettings.pauseHelp)).toBeVisible()
|
||||
expect(screen.queryByLabelText(new RegExp(copy.reviewSettings.executionSeconds))).not.toBeInTheDocument()
|
||||
expect(copy.reviewSettings.pauseHelp).toContain(locale === 'zh-CN' ? '持续处理至完成' : 'continuously until complete')
|
||||
expect(i18n.exists('reviewSettings.pauseHelp', { ns: 'heartbeat', lng: locale })).toBe(true)
|
||||
expect(JSON.stringify(copy)).not.toMatch(/RPM|TPM|Retry-After|尚未实现|not implemented|没有按时间保存的操作审计|chronological audit log|暂不支持按分钟|Minute intervals are not supported/)
|
||||
expect(copy.timeouts.concurrencyHelp).toContain(locale === 'zh-CN' ? '仅限制监督请求' : 'This limits supervision only')
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ApplicationSettings, ApplicationSettingsUpdate } from '../../share
|
||||
import { defaultSupervisionReviewSettings, supervisionReviewSettingsSchema } from '../../shared/supervision-review-contracts'
|
||||
|
||||
const fields = [
|
||||
['pageSize', 1, 200], ['batchCharacters', 1000, 16000], ['batchMessages', 1, 50], ['executionSeconds', 30, 3600], ['responseKiB', 100, 16384]
|
||||
['pageSize', 1, 200], ['batchCharacters', 1000, 16000], ['batchMessages', 1, 50], ['responseKiB', 100, 16384]
|
||||
] as const
|
||||
|
||||
export function SupervisionReviewSettings({ settings, disabled, onSave }: {
|
||||
|
||||
@@ -9,7 +9,7 @@ export const heartbeat = {
|
||||
help: 'Page size controls database reads, not total coverage. Batch limits split work without dropping remaining text. These settings and the organize timeout are frozen for each new review; continuing uses its saved configuration. Response capacity uses the current setting for each request; increase it before continuing a failed review.',
|
||||
pageSize: 'Sources read at a time (default 50)', batchCharacters: 'Text size per batch (default 8000)',
|
||||
batchMessages: 'Messages per batch (default 20)', executionSeconds: 'Time before pausing, seconds (default 300)',
|
||||
pauseHelp: 'When the execution soft budget is reached, active batches finish and save before pausing. Continue from Activity records.',
|
||||
pauseHelp: 'Reviews run continuously until complete and save each batch. Pause and continue from Activity records.',
|
||||
invalid: 'Enter whole numbers within the displayed ranges.',
|
||||
pause: 'Pause review', resume: 'Continue saved review', facts: 'Retained facts and sources',
|
||||
pausedHint: 'Review is incomplete. Successful batches are saved. Open Activity to inspect progress and continue.',
|
||||
@@ -19,7 +19,7 @@ export const heartbeat = {
|
||||
previousPage: 'Previous page', nextPage: 'Next page',
|
||||
extracting: 'Reviewing source batches', summarizing: 'Building navigation summaries', inFlight: '{{count}} queued or running model tasks',
|
||||
configuration: 'Saved review configuration',
|
||||
configurationValues: 'Read {{pageSize}} sources at a time; batch size {{batchCharacters}}, up to {{batchMessages}} messages; pause after {{executionSeconds}}s; model timeout {{timeoutSeconds}}s; up to {{concurrency}} simultaneous tasks. Tasks also share the current supervision concurrency limit.',
|
||||
configurationValues: 'Read {{pageSize}} sources at a time; batch size {{batchCharacters}}, up to {{batchMessages}} messages; run continuously until complete; model timeout {{timeoutSeconds}}s; up to {{concurrency}} simultaneous tasks. Tasks also share the current supervision concurrency limit.',
|
||||
progress: '{{batches}} saved batches · {{characters}} characters reviewed · {{remaining}} sources remaining',
|
||||
identityError: 'The model returned an invalid entity reference. This batch was not saved; previously saved batches remain available.',
|
||||
runError: 'This run failed. Review the error details before continuing; saved batches remain available.',
|
||||
|
||||
@@ -6,7 +6,7 @@ export const heartbeat = {
|
||||
help: '每页条数只控制数据库读取量,不限制回顾总量。分批上限用于拆分工作,剩余正文会继续处理。这些设置及整理超时在新回顾开始时固定;继续回顾沿用已保存的配置。响应容量按每次请求的当前设置生效,调高后可继续失败的回顾。',
|
||||
pageSize: '每次读取来源条数(默认 50)', batchCharacters: '每批文本量(默认 8000)',
|
||||
batchMessages: '每批消息数(默认 20)', executionSeconds: '单次整理时长,秒(默认 300)',
|
||||
pauseHelp: '执行时间达到软预算后,正在处理的批次完成并保存,再暂停;可从活动记录继续。',
|
||||
pauseHelp: '回顾持续处理至完成,每批保存进度;可从活动记录主动暂停并继续。',
|
||||
invalid: '请输入所示范围内的整数。',
|
||||
pause: '暂停回顾', resume: '继续已保存回顾', facts: '已保留事实与来源',
|
||||
pausedHint: '本次回顾尚未完成,成功批次已保存。请到活动页查看进度并继续。',
|
||||
@@ -16,7 +16,7 @@ export const heartbeat = {
|
||||
previousPage: '上一页', nextPage: '下一页',
|
||||
extracting: '正在整理来源批次', summarizing: '正在生成导航摘要', inFlight: '{{count}} 个模型任务排队或执行中',
|
||||
configuration: '本次回顾的已保存配置',
|
||||
configurationValues: '每次读取 {{pageSize}} 个来源;每批文本量 {{batchCharacters}},最多 {{batchMessages}} 条消息;整理 {{executionSeconds}} 秒后暂停;模型超时 {{timeoutSeconds}} 秒;最多同时处理 {{concurrency}} 个任务。实际并发还受当前监督模型并发设置限制。',
|
||||
configurationValues: '每次读取 {{pageSize}} 个来源;每批文本量 {{batchCharacters}},最多 {{batchMessages}} 条消息;持续处理至完成;模型超时 {{timeoutSeconds}} 秒;最多同时处理 {{concurrency}} 个任务。实际并发还受当前监督模型并发设置限制。',
|
||||
progress: '已保存 {{batches}} 批 · 已读 {{characters}} 个字符 · 剩余 {{remaining}} 个来源',
|
||||
identityError: '模型返回的实体引用无效,本批未保存,之前成功的批次仍保留。',
|
||||
runError: '本次运行失败,请查看错误详情后继续。已保存的批次仍保留。',
|
||||
|
||||
Reference in New Issue
Block a user