feat: make heartbeat trigger incremental reviews and suggest from graph changes

Heartbeat no longer writes its own report; it triggers the shared incremental review and, per plan intervention, derives evidence-backed suggestions from the review's graph changes. Manual review is incremental by default with explicit reanalysis. The Supervisor tab is renamed Smart heartbeat with heartbeat plans, and the redundant inner plan frame is removed.
This commit is contained in:
mesalogo
2026-09-30 10:45:22 +08:00
parent d30f794184
commit 6293ef105d
51 changed files with 2056 additions and 2150 deletions
+8 -1
View File
@@ -949,6 +949,12 @@ export const heartbeatScopeSchema = z.discriminatedUnion('kind', [
.strict()
])
// How the supervisor intervenes after an automatic review. Real-time
// conversation intervention is not offered until it is implemented.
export const heartbeatInterventionSchema = z.enum(['suggest', 'memory'])
export type HeartbeatIntervention = z.infer<typeof heartbeatInterventionSchema>
export const defaultHeartbeatIntervention: HeartbeatIntervention = 'suggest'
export const heartbeatCreateSchema = z
.object({
scope: heartbeatScopeSchema,
@@ -957,7 +963,8 @@ export const heartbeatCreateSchema = z
recurrence: heartbeatRecurrenceSchema,
enabled: z.boolean(),
lookbackHours: z.number().int().min(1).max(24 * 30),
retentionDays: z.number().int().min(1).max(365)
retentionDays: z.number().int().min(1).max(365),
intervention: heartbeatInterventionSchema.optional()
})
.strict()
+3
View File
@@ -2221,6 +2221,9 @@ export type DesktopApi = {
knowledgeCommit: (input: import('./supervision-contracts').SupervisionKnowledgeCommitRequest) => Promise<Record<string, unknown>>
entityAction: (input: import('./supervision-contracts').SupervisionEntityAction) => Promise<void>
relationAction: (input: import('./supervision-contracts').SupervisionRelationAction) => Promise<void>
suggestions: (input?: { status?: 'pending' | 'all'; limit?: number; offset?: number }) => Promise<import('./supervision-contracts').SupervisionSuggestion[]>
suggestionAction: (input: import('./supervision-contracts').SupervisionSuggestionAction) => Promise<import('./supervision-contracts').SupervisionSuggestion>
retrySuggestions: (input: { heartbeatRunId: string }) => Promise<number>
}
experts: {
list: () => Promise<AssistantExpert[]>
+3
View File
@@ -273,6 +273,9 @@ export const ipcChannels = {
supervisionActivity: 'supervision:activity',
supervisionPause: 'supervision:pause',
supervisionCancel: 'supervision:cancel',
supervisionSuggestions: 'supervision:suggestions',
supervisionSuggestionAction: 'supervision:suggestion-action',
supervisionRetrySuggestions: 'supervision:retry-suggestions',
supervisionExecution: 'supervision:execution',
supervisionResume: 'supervision:resume',
supervisionBatches: 'supervision:batches',
+42 -1
View File
@@ -18,12 +18,49 @@ export const supervisionRunRequestSchema = z
.object({
trigger: supervisionTriggerSchema,
scope: heartbeatScopeSchema,
timeRange: supervisionTimeRangeSchema
timeRange: supervisionTimeRangeSchema,
// Reviews skip processed, unchanged sources by default. Only an explicit
// user re-analysis reads the whole interval again; it never moves progress.
reanalyze: z.boolean().optional()
})
.strict()
export type SupervisionRunRequest = z.infer<typeof supervisionRunRequestSchema>
/** Whether a review reads and advances the shared incremental progress. */
export function isIncrementalReview(request: Pick<SupervisionRunRequest, 'trigger' | 'reanalyze'>): boolean {
return request.trigger === 'heartbeat' || request.reanalyze !== true
}
export const supervisionSuggestionKindSchema = z.enum(['open_item', 'conflict', 'convention', 'revision'])
export type SupervisionSuggestionKind = z.infer<typeof supervisionSuggestionKindSchema>
export type SupervisionSuggestion = {
id: string
resultId: string | null
heartbeatRunId: string | null
scope: SupervisionRunRequest['scope']
kind: SupervisionSuggestionKind
title: string
detail: string
sourceIds: string[]
entityId: string | null
relationId: string | null
taskId: string | null
status: 'pending' | 'accepted' | 'dismissed'
createdAt: string
}
export const supervisionSuggestionListRequestSchema = z.object({
status: z.enum(['pending', 'all']).default('pending'),
limit: z.number().int().min(1).max(100).default(50),
offset: z.number().int().min(0).max(100_000).default(0)
}).strict()
export const supervisionSuggestionActionSchema = z.object({
id: z.string().uuid(),
action: z.enum(['accept', 'dismiss'])
}).strict()
export type SupervisionSuggestionAction = z.infer<typeof supervisionSuggestionActionSchema>
export const supervisionSuggestionRetrySchema = z.object({ heartbeatRunId: z.string().min(1).max(256) }).strict()
export const supervisionActivityRequestSchema = z.object({
configId: z.string().min(1).max(256).optional(),
limit: z.number().int().min(1).max(100).default(50),
@@ -45,6 +82,10 @@ export type SupervisionActivity = {
resultId: string | null
heartbeatStatus: 'claimed' | 'completed' | 'failed' | 'skipped' | 'no_change' | null
supervisionStatus: 'running' | 'completed' | 'failed' | 'no_change' | 'paused' | 'cancelled' | null
/** Intervention after an automatic review; null when not applicable. */
suggestionStatus?: 'running' | 'completed' | 'failed' | 'skipped' | null
suggestionError?: string | null
suggestionCount?: number
}
export const supervisionEvidenceSchema = z