fix: resolve runtime, review, channel and chat state failures

Keep remote startup cancellable without blocking other sessions and surface local OpenCode submission failures promptly. Preserve supervisor project isolation, pause behavior and valid story destinations.

Restore generated-image channel delivery, Telegram reconnection on unchanged Save and DingTalk business-error retries. Preserve virtualized message disclosures using sparse state and release stale supervisor UI requests.

Add production-path regressions, Electron checks, isolated SQLite validation and audit evidence. Fix the SDK cleanup test fixture lifetime under garbage collection without weakening its assertions.

Validation: 6136 tests passed, 87 skipped; typecheck, lint and diff checks passed. Seven real text-model calls, current-source Linux x64 Host scenarios and large-history Electron comparisons completed.

Release note: Fix stuck runtime requests, inconsistent review results and channel delivery failures, and retain expanded details while browsing long conversations. Remote runtime fixes require an Agent build containing these changes.
This commit is contained in:
mesalogo
2026-10-05 10:25:43 +08:00
parent 126dbc57e2
commit bbf24c282a
49 changed files with 1710 additions and 106 deletions
+3 -3
View File
@@ -12378,11 +12378,11 @@ export class AssistantDatabase {
return { summary: this.reviewSummary(request.scope, 'supervisor'), background: this.reviewBackground(request.scope) }
}
async supervisionCandidates(request: SupervisionRunRequest): Promise<ReturnType<AssistantDatabase['listSupervisionCandidates']>> {
async supervisionCandidates(request: SupervisionRunRequest, batch?: { projectId: string; crossProject: boolean }): Promise<ReturnType<AssistantDatabase['listSupervisionCandidates']>> {
const reader = this.readonlyQueryReader()
if (reader) return reader.call('reviewCandidates', [request])
if (reader) return reader.call('reviewCandidates', [request, batch])
if (this.databasePath !== ':memory:') throw new Error('Supervision worker is not configured')
return this.listSupervisionCandidates(request)
return this.listSupervisionCandidates(request, batch)
}
supervisionSuggestions(): SupervisionSuggestionStore {
@@ -211,7 +211,17 @@ function fixture(content = 'Atlas and Beacon are separate projects.') {
const service = (responseKiB = 1024) => createProductionSupervisorService(db, async () => ({ supervisorModelConcurrency: 1,
supervisionReview: { pageSize: 10, batchCharacters: 1000, batchMessages: 10, executionSeconds: 300, responseKiB } }),
async () => ({ runtimeId: 'model', capability: 'chat', run } as AgentRuntime), pool)
return { db, request, respond, service }
// Project ownership comes from timeline events, not the scope that first saved an entity.
const associateCandidates = () => {
const sql = (db as unknown as { requireDatabase(): DatabaseSync }).requireDatabase()
const id = randomUUID()
sql.prepare(`INSERT INTO supervision_events (id, story_line_id, result_id, occurred_at, title, description, event_type, project_id)
SELECT ?, story_lines.id, supervision_results.id, ?, 'Seed ownership', '', 'discussion', ?
FROM story_lines, supervision_results LIMIT 1`).run(id, request.timeRange.from, db.listProjects()[0]!.id)
sql.prepare('INSERT INTO supervision_event_entities (event_id, entity_id, change_type) SELECT ?, id, ? FROM supervision_entities').run(id, 'associated')
sql.prepare('INSERT INTO supervision_story_assigned VALUES (?)').run(id)
}
return { db, request, respond, service, associateCandidates }
}
it('records supervisor model usage under a hidden supervision task', async () => {
@@ -237,6 +247,7 @@ it('production factory normalizes two absent identities, then reuses exact candi
const first = await f.service().run(f.request)
expect(first.output.entities).toHaveLength(2)
for (const entity of first.output.entities) expect(entity).not.toHaveProperty('persistedId')
f.associateCandidates()
const candidates = f.db.listSupervisionCandidates(f.request)
expect(candidates).toHaveLength(2)
expect(f.respond.mock.calls[0]![0]).toContain('KNOWN ENTITIES is empty. Every entity is new; omit candidateRef')
@@ -256,6 +267,7 @@ it('retains candidate mappings across leaf batches, failed merge and resume with
const f = fixture('Atlas and Beacon. '.repeat(100))
f.db.saveSupervisionResult({ request: f.request, evidence: [], output: { ...empty,
entities: ['Atlas', 'Beacon'].map(label => ({ id: label, label, description: label, sourceReferenceIds: [] })) } })
f.associateCandidates()
const candidates = f.db.listSupervisionCandidates(f.request)
const original = f.respond.getMockImplementation()!
let failMerge = true
@@ -346,6 +358,7 @@ it('reuses actual legacy text-key shapes through strict UUID aliases and preserv
sql.prepare(`INSERT INTO supervision_entities (id, story_line_id, canonical_label, description, confirmation_state, updated_at, source_reference_ids_json)
VALUES (?, ?, ?, 'Human description', 'confirmed', ?, '[]')`).run(id, String(story.id), id, new Date().toISOString())
}
f.associateCandidates()
const candidates = f.db.listSupervisionCandidates(f.request)
const legacy = candidates.filter(candidate => candidate.storageId)
expect(legacy).toHaveLength(2)
+1 -1
View File
@@ -126,7 +126,7 @@ export function createProductionSupervisorService(
background: request => database.reviewBackground(request.scope),
start: (request, heartbeatRunId) => database.startSupervisionRun(request, heartbeatRunId),
fail: (runId, error) => database.failSupervisionRun(runId, error), noChange: runId => database.noChangeSupervisionRun(runId),
candidates: request => database.supervisionCandidates(request), save: async result => database.saveSupervisionResult(result)
candidates: (request, batch) => database.supervisionCandidates(request, batch), save: async result => database.saveSupervisionResult(result)
}, { database: () => database.supervisionReviewStore(),
initialize: (runId, state, signal) => database.initializeSupervisionReview(runId, state, signal),
resume: (runId, signal) => database.resumeSupervisionReview(runId, signal),
@@ -247,3 +247,37 @@ it('runs after publication inside the review and records failure without failing
expect(f.stories.list({ kind: 'global' }).map(story => story.name)).toEqual(['Timeline'])
await expect(service.run(request)).resolves.toMatchObject({ status: 'no_change', coverage: { stories: { status: 'completed', calls: 0 } } })
})
it.each(['remove', 'merge'] as const)('rejects an assignment whose selected story was changed by %s while awaiting the model', async action => {
const f = await fixture()
f.publish(f.a.id, ['First', 'Second'])
await assignStories(f.stories, answer({ stories: [{ key: 'new_1', level: 'feature', name: 'Alpha' }, { key: 'new_2', level: 'feature', name: 'Beta' }],
assignments: [{ event: 'e_1', story: 'new_1' }, { event: 'e_2', story: 'new_2' }] }), { kind: 'global' }, options)
const [alpha, beta] = f.stories.list({ kind: 'global' })
f.publish(f.a.id, ['New evidence'], 22)
const model = async () => {
f.stories.act(action === 'remove' ? { action, storyId: alpha!.id } : { action, storyId: alpha!.id, intoId: beta!.id })
return JSON.stringify({ stories: [{ key: 'new_1', level: 'feature', name: 'Must roll back' }], assignments: [{ event: 'e_1', story: 'story_1' }] })
}
await expect(assignStories(f.stories, model, { kind: 'global' }, options)).rejects.toThrow('Story changed')
expect(f.stories.pending({ kind: 'global' })).toHaveLength(1)
expect(f.stories.list({ kind: 'global' }).map(story => story.name)).toEqual(['Beta'])
expect(f.sql.prepare("SELECT COUNT(*) AS n FROM supervision_event_stories es JOIN supervision_stories s ON s.id = es.story_id WHERE s.status != 'current'").get()!.n).toBe(0)
await assignStories(f.stories, answer({ assignments: [{ event: 'e_1', story: 'story_1' }] }), { kind: 'global' }, { ...options, threadEvents: 100 })
expect(f.stories.pending({ kind: 'global' })).toEqual([])
expect(f.sql.prepare('PRAGMA foreign_key_check').all()).toEqual([])
})
it.each(['feature', 'thread'] as const)('rejects a split after its %s was removed without leaving new threads', async removed => {
const f = await fixture()
f.publish(f.a.id, ['One', 'Two', 'Three', 'Four'])
await assignStories(f.stories, answer({ stories: [{ key: 'new_1', level: 'feature', name: 'Feature' }, { key: 'new_2', level: 'thread', name: 'Thread', parent: 'new_1' }],
assignments: [1, 2, 3, 4].map(n => ({ event: `e_${n}`, story: n === 4 ? 'new_2' : 'new_1' })) }), { kind: 'global' }, { ...options, threadEvents: 4 })
const [feature, thread] = f.stories.list({ kind: 'global' })
const input = f.stories.splitInput(feature!.id, 8000)
f.stories.act({ action: 'remove', storyId: removed === 'feature' ? feature!.id : thread!.id })
const before = f.stories.list({ kind: 'global' })
expect(() => f.stories.split(input, { threads: [], moves: [{ event: 'e_1', thread: 'thread_1' }] }, 4)).toThrow('Story changed')
expect(f.stories.list({ kind: 'global' })).toEqual(before)
expect(f.sql.prepare('PRAGMA foreign_key_check').all()).toEqual([])
})
+15
View File
@@ -236,6 +236,7 @@ export class SupervisionStoryStore {
VALUES (?, ?, ?, 'thread', ?, ?, ?, ?)`).run(id, String(input.feature.project_id), featureId, thread.name, thread.description, now, now)
ids.set(thread.key, id); existing.set(name, id); created++
}
this.assertCurrent([featureId, ...output.moves.flatMap(item => ids.get(item.thread) ?? [])])
const move = this.db.prepare('UPDATE supervision_event_stories SET story_id = ? WHERE event_id = ? AND story_id = ? AND is_primary = 1 AND user_set = 0')
for (const item of output.moves) {
const id = ids.get(item.thread)
@@ -252,6 +253,14 @@ export class SupervisionStoryStore {
.map(row => `${row.parent}|${nameKey(String(row.name))}`))
}
/** Check model-selected destinations under the caller's write transaction. */
private assertCurrent(ids: string[]): void {
const unique = [...new Set(ids)]
const count = this.db.prepare(`SELECT COUNT(*) AS n FROM supervision_stories
WHERE id IN (SELECT value FROM json_each(?)) AND status = 'current'`).get(JSON.stringify(unique))!.n
if (Number(count) !== unique.length) throw new Error('Story changed while organizing; retry with current stories')
}
/** Applies one validated model answer for one project chunk in a single transaction. */
apply(projectId: string, events: PendingEvent[], candidates: Row[], output: z.infer<typeof outputSchema>, options: Pick<StoryAssignmentOptions, 'crossProject' | 'threadEvents'>, others: Row[] = []): Omit<StoryAssignmentResult, 'calls' | 'split'> {
const eventRefs = new Map(events.map((event, index) => [`e_${index + 1}`, event]))
@@ -336,6 +345,12 @@ export class SupervisionStoryStore {
ids.set(story.key, id)
created++
}
const selected = [
...output.assignments.flatMap(item => [item.story, ...(item.related ?? [])]),
...output.stories.map(item => item.parent), ...output.concluded.map(item => item.story),
...output.links.flatMap(item => [item.cross, item.feature])
]
this.assertCurrent(selected.flatMap(ref => ref ? ids.get(ref) ?? (otherRefs.has(ref) ? String(otherRefs.get(ref)!.id) : []) : []))
const link = this.db.prepare('INSERT OR IGNORE INTO supervision_event_stories (event_id, story_id, is_primary) VALUES (?, ?, ?)')
const mark = this.db.prepare('INSERT OR IGNORE INTO supervision_story_assigned (event_id) VALUES (?)')
let assigned = 0, unassigned = 0
+3 -3
View File
@@ -73,9 +73,9 @@ export function supersedeReextractedEvents(db: DatabaseSync, resultId: string):
*/
export function timelineCandidates(db: DatabaseSync, projectId: string, crossProject: boolean): Array<{ id: string; label: string; description: string }> {
return db.prepare(`SELECT e.id, e.canonical_label AS label, e.description FROM supervision_entities e
WHERE e.confirmation_state != 'revoked' AND (? = 1 OR EXISTS (
SELECT 1 FROM supervision_event_entities ee JOIN supervision_events ev ON ev.id = ee.event_id
WHERE ee.entity_id = e.id AND ev.superseded_by IS NULL AND COALESCE(ev.project_id, '') = ?))
WHERE e.confirmation_state != 'revoked' AND (? = 1 OR e.id IN (
SELECT ee.entity_id FROM supervision_events ev JOIN supervision_event_entities ee ON ee.event_id = ev.id
WHERE ev.superseded_by IS NULL AND COALESCE(ev.project_id, '') = ?))
ORDER BY e.updated_at DESC, e.id LIMIT 100`).all(crossProject ? 1 : 0, projectId) as Array<{ id: string; label: string; description: string }>
}
@@ -157,6 +157,76 @@ it('publishes full worker-selected coverage atomically and skips it on the next
expect(calls).toBe(before)
})
it.each([false, true])('production worker preserves batch candidate isolation with crossProject=%s', async crossProject => {
const f = await fixture()
const empty = { summary: 'Seed', changeDigest: '', openItems: [], events: [], entities: [], entityChanges: [], relations: [] }
const projects = f.db.listProjects()
for (const project of projects) f.db.saveSupervisionResult({
request: { ...f.state.request, scope: project.id === f.project.id ? { kind: 'global' } : { kind: 'projects', projectIds: [project.id] } },
evidence: [{ id: 's', sourceType: 'conversation', sourceId: randomUUID(), title: 'Seed', content: 'Seed',
occurredAt: f.state.request.timeRange.from, locator: { projectId: project.id } }],
output: { ...empty, entities: [{ id: 'entity', label: project.id, description: 'Owned knowledge', sourceReferenceIds: ['s'] }],
events: [{ title: 'Seed', description: '', occurredAt: f.state.request.timeRange.from, eventType: 'discussion', entityIds: ['entity'], sourceReferenceIds: ['s'] }] }
})
f.sql.exec('INSERT INTO supervision_story_assigned SELECT id FROM supervision_events')
const pool = new SupervisionModelPool()
cleanup.push(async () => pool.dispose())
const seen = new Set<string>()
const runtime = { async *run(input: { prompt: string }) {
if (!input.prompt.includes('These inputs are navigation summaries')) {
const evidence = JSON.parse(input.prompt.split('BOUNDED EVIDENCE:\n\n')[1]!.split('\n\nReturn only JSON.')[0]!) as Array<{ locator: { projectId: string } }>
const projectId = evidence[0]!.locator.projectId
const candidates = JSON.parse(input.prompt.split('KNOWN ENTITIES:\n\n')[1]!.split('\n\nPREVIOUS SUMMARY')[0]!) as Array<{ label: string }>
expect(candidates.map(item => item.label).sort()).toEqual((crossProject ? projects.map(p => p.id) : [projectId]).sort())
seen.add(projectId)
}
yield { type: 'text', delta: JSON.stringify(empty) }
yield { type: 'done' }
}, async dispose() {} } as unknown as AgentRuntime
const service = createProductionSupervisorService(f.db, async () => ({ supervisionReview: supervisionReviewSettingsSchema.parse({ crossProject }) }), async () => runtime, pool)
await expect(service.run(f.state.request)).resolves.toMatchObject({ status: 'completed', coverage: { sources: 422 } })
expect([...seen].sort()).toEqual(projects.map(p => p.id).sort())
expect(f.sql.prepare('SELECT COUNT(*) AS n FROM supervision_entities').get()!.n).toBe(2)
expect(f.sql.prepare('PRAGMA foreign_key_check').all()).toEqual([])
})
it.each(['paused', 'cancelled'] as const)('honors %s during the final production candidate read before publishing', async action => {
const f = await fixture()
const pool = new SupervisionModelPool()
cleanup.push(async () => pool.dispose())
const runtime = { async *run() {
yield { type: 'text', delta: JSON.stringify({ summary: 'Saved leaf', changeDigest: '', openItems: [], events: [], entities: [], entityChanges: [], relations: [] }) }
yield { type: 'done' }
}, async dispose() {} } as unknown as AgentRuntime
const service = createProductionSupervisorService(f.db, async () => ({}), async () => runtime, pool)
const candidates = f.db.supervisionCandidates.bind(f.db)
let stopped = false
let cancellation: Promise<void> | undefined
vi.spyOn(f.db, 'supervisionCandidates').mockImplementation(async (...args) => {
const result = await candidates(...args)
const runId = service.execution().runId!
if (!stopped && f.db.supervisionReviewStore().progress(runId).phase === 'saving') {
stopped = true
if (action === 'paused') service.pause(runId)
else cancellation = service.cancel(runId)
}
return result
})
const result = await service.run(f.state.request)
await cancellation
expect(stopped).toBe(true)
expect(result.status).toBe(action)
expect(f.db.listSupervisionResults()).toEqual([])
expect(f.sql.prepare('SELECT COUNT(*) AS n FROM review_checkpoints').get()!.n).toBe(0)
const saved = f.db.supervisionReviewStore().batches(result.runId!, 1000)
expect(saved.length).toBeGreaterThan(0)
if (action === 'paused') {
await expect(service.resume(result.runId!)).resolves.toMatchObject({ status: 'completed' })
expect(f.db.supervisionReviewStore().batches(result.runId!, 1000)).toEqual(saved)
expect(f.sql.prepare('SELECT COUNT(*) AS n FROM review_checkpoints').get()!.n).toBe(422)
}
})
it.each(['crash', 'close'] as const)('settles writer %s before releasing pending work and can rebuild after reopening', async action => {
const f = await fixture()
const id = f.db.startSupervisionRun(f.state.request)
+1
View File
@@ -487,6 +487,7 @@ export class SupervisorService {
for (const batch of saved) savedProjects.add(batch.projectId)
}
for (const projectId of savedProjects) await candidatesFor(projectId)
controller.signal.throwIfAborted()
const result: StoredSupervisionResult = { runId, request, candidates: [...offered.values()],
evidence: progress.batches === 1 ? first!.evidence : [], output: root!.output,
status: 'completed', coverage: { ...db.progress(runId), complete: true } }