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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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[]>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user