fix: synchronize graph review selection and attention radii
Share review history selection between overview and graph while preserving explicit navigation and stale-response guards. Return attention for the selected review scope and interval, and align spiral events and time with that review. Normalize radii from message rates, including partial turns, without lifting quiet turns toward peaks. Preserve one continuous helix, uniform time height, round turn sections, and smooth transitions; equal densities retain equal radii. Document current-message and hourly-bucket limitations. Validation: 51 focused unit tests, 2 production IPC tests, Electron DOM and geometry checks, Node/Agent/Web typecheck, targeted ESLint, and diff checks passed. Release note: Story graphs now offer synchronized review history selection, and spiral radii reflect recorded message density within the selected review.
This commit is contained in:
@@ -9308,10 +9308,17 @@ export class AssistantDatabase {
|
||||
if (!story) return { storyLine: null, events: [], entities: [], relations: [], sources: [], eventEntities: [], eventSources: [] }
|
||||
const id = (story as { id: string }).id
|
||||
if (input.resultId) {
|
||||
const row = database.prepare('SELECT graph_snapshot_json FROM supervision_results WHERE id = ?').get(input.resultId) as { graph_snapshot_json: string }
|
||||
const row = database.prepare(`SELECT r.graph_snapshot_json, sr.scope_json, sr.time_range_json
|
||||
FROM supervision_results r JOIN supervision_runs sr ON sr.id = r.run_id WHERE r.id = ?`).get(input.resultId) as {
|
||||
graph_snapshot_json: string; scope_json: string; time_range_json: string
|
||||
}
|
||||
const snapshot = JSON.parse(row.graph_snapshot_json) as { entities: unknown[]; relations: Array<{ confirmation_state: string }> }
|
||||
const scope = JSON.parse(row.scope_json) as SupervisionRunRequest['scope']
|
||||
const range = JSON.parse(row.time_range_json) as SupervisionRunRequest['timeRange']
|
||||
return {
|
||||
storyLine: story, ...snapshot, relations: snapshot.relations.filter((relation) => relation.confirmation_state !== 'revoked'),
|
||||
attention: supervisionAttention(database, scope.kind === 'projects' ? scope.projectIds : undefined,
|
||||
new Date(range.from).toISOString(), new Date(range.to).toISOString()),
|
||||
events: database.prepare('SELECT * FROM supervision_events WHERE result_id = ? ORDER BY occurred_at').all(input.resultId),
|
||||
sources: database.prepare('SELECT * FROM supervision_sources WHERE result_id = ?').all(input.resultId),
|
||||
eventEntities: database.prepare(`SELECT ee.event_id, ee.entity_id FROM supervision_event_entities ee JOIN supervision_events e ON e.id = ee.event_id WHERE e.result_id = ?`).all(input.resultId),
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ASSISTANT_DATABASE_SCHEMA_VERSION, AssistantDatabase } from './assistan
|
||||
import { SupervisorService, type SupervisorSummarizerRequest } from './supervisor-service'
|
||||
import type { SupervisionRunRequest } from '../../shared/supervision-contracts'
|
||||
import type { ReviewConfiguration } from './supervision-review-store'
|
||||
import { supervisionGraphViewSchema } from '../../shared/supervision-contracts'
|
||||
|
||||
const cleanups: Array<() => Promise<void>> = []
|
||||
afterEach(async () => { for (const cleanup of cleanups.splice(0)) await cleanup() })
|
||||
@@ -53,6 +54,24 @@ async function fixture(crossProject = false) {
|
||||
|
||||
const current = (sql: DatabaseSync) => sql.prepare('SELECT * FROM supervision_events WHERE superseded_by IS NULL ORDER BY started_at').all()
|
||||
|
||||
it('supplies real message attention on the review-specific graph, bounded by its project and time range', async () => {
|
||||
const f = await fixture()
|
||||
await f.run({ kind: 'projects', projectIds: [f.a.id] })
|
||||
const result = f.db.listSupervisionResults()[0]!
|
||||
// Additional messages outside the selected review must not expand its attention span.
|
||||
f.db.saveLocalConversations([{ header: { id: randomUUID(), projectId: f.a.id, title: 'Outside review', updatedAt: base },
|
||||
messages: [
|
||||
{ id: randomUUID(), role: 'assistant', content: 'Before', createdAt: Date.parse('2026-09-18T08:00:00Z'), state: 'complete' },
|
||||
{ id: randomUUID(), role: 'assistant', content: 'Reply', createdAt: Date.parse('2026-09-20T08:15:00Z'), state: 'complete' },
|
||||
{ id: randomUUID(), role: 'assistant', content: 'Outside', createdAt: Date.parse('2026-09-23T08:00:00Z'), state: 'complete' }
|
||||
] }])
|
||||
const graph = supervisionGraphViewSchema.parse(f.db.getSupervisionGraph({ resultId: result.id, storyLineId: result.storyLineId }))
|
||||
expect(graph.attention).toEqual([
|
||||
{ start: '2026-09-20T08:00:00.000Z', turns: 2, characters: 35 },
|
||||
{ start: '2026-09-20T09:00:00.000Z', turns: 1, characters: 31 }
|
||||
])
|
||||
})
|
||||
|
||||
it('extracts each source once for every scope: a project review then global reads only the other project', async () => {
|
||||
const f = await fixture()
|
||||
await f.run({ kind: 'projects', projectIds: [f.a.id] })
|
||||
|
||||
@@ -6646,6 +6646,7 @@ describe('registerIpcHandlers agent terminal state', () => {
|
||||
expect(database.getSupervisionSource(overview[0]!.sourceId)?.sourceId).toBe(conversationId)
|
||||
const graph = await electronMocks.handlers.get(ipcChannels.supervisionGraph)!(trustedEvent(harness.webContents), { resultId: overview[0]!.id, storyLineId: overview[0]!.storyLineId }) as { sources: unknown[] }
|
||||
expect(graph.sources).toHaveLength(evidence.length)
|
||||
expect(graph).toHaveProperty('attention', [{ start: from, turns: 2, characters: 14 }])
|
||||
expect(() => electronMocks.handlers.get(ipcChannels.supervisionGraph)!(trustedEvent(harness.webContents), { resultId: overview[0]!.id, storyLineId: 'wrong-story' })).toThrow('不匹配')
|
||||
expect(() => electronMocks.handlers.get(ipcChannels.supervisionContinueContext)!(trustedEvent(harness.webContents), { resultId: 'wrong-result', sourceId: overview[0]!.sourceId })).toThrow('不匹配')
|
||||
const legacy = database.buildHeartbeatInput({ scope, lookbackHours: 1 }, new Date(to))
|
||||
|
||||
Reference in New Issue
Block a user