feat: add supervisor workspace and traceable story graph

Extend the existing heartbeat workspace with scoped work reviews, result-specific story graphs, source inspection, and pinned conversation or task feedback. Connect confirmed continuation and local knowledge entity previews through the desktop APIs, and preserve result history and user corrections in schema 43.

Include focused regression tests, an isolated visual fixture, and implementation evidence. Full event-by-event replay, Experiment targets, no-change handling, and cancellation remain incomplete; this commit does not claim complete feature acceptance. Existing automatic plans remain active when the application entry is disabled.

Release note: Review work progress in the Supervisor workspace, trace graph events to saved sources, and open a matching review from pinned conversation or task feedback.
This commit is contained in:
mesalogo
2026-09-23 13:49:13 +08:00
parent 785b1ee468
commit 7d487f9326
49 changed files with 5980 additions and 310 deletions
+7 -3
View File
@@ -33,17 +33,19 @@ export type UpdateSource = z.infer<typeof updateSourceSchema>
export const builtInApplicationIds = ['magic-notes', 'knowledge', 'heartbeat', 'local-inference'] as const
export type BuiltInApplicationId = typeof builtInApplicationIds[number]
export const editableApplicationIds = ['magic-notes', 'local-inference'] as const
export const editableApplicationIds = ['magic-notes', 'heartbeat', 'local-inference'] as const
export type EditableApplicationId = typeof editableApplicationIds[number]
export const defaultApplicationNavigation = {
order: ['knowledge', 'heartbeat', 'magic-notes', 'local-inference'] as BuiltInApplicationId[],
pinned: { 'magic-notes': true, 'local-inference': false }
pinned: { 'magic-notes': true, heartbeat: true, 'local-inference': false }
}
export const applicationNavigationSchema = z.object({
order: z.array(z.enum(builtInApplicationIds)).length(builtInApplicationIds.length)
.refine(ids => new Set(ids).size === builtInApplicationIds.length, 'Each application must occur exactly once'),
pinned: z.object({
'magic-notes': z.boolean(), 'local-inference': z.boolean()
'magic-notes': z.boolean(),
heartbeat: z.boolean().default(true),
'local-inference': z.boolean()
}).strict()
}).strict()
@@ -58,6 +60,7 @@ const applicationPreferencesSchema = z
applicationNavigation: applicationNavigationSchema.default(defaultApplicationNavigation),
localInferenceEnabled: z.boolean().default(true),
magicNotesEnabled: z.boolean().default(true),
heartbeatEnabled: z.boolean().optional(),
magicNotesShowIncompleteTodoCount: z.boolean().default(true),
magicNoteCommentMode: magicNoteCommentModeSchema.default('immediate'),
magicNoteCommentFormat: magicNoteCommentFormatSchema.default('combined'),
@@ -77,6 +80,7 @@ export const applicationSettingsUpdateSchema = applicationPreferencesSchema
applicationNavigation: applicationNavigationSchema.optional(),
localInferenceEnabled: z.boolean().optional(),
magicNotesEnabled: z.boolean().optional(),
heartbeatEnabled: z.boolean().optional(),
magicNotesShowIncompleteTodoCount: z.boolean().optional(),
magicNoteCommentMode: magicNoteCommentModeSchema.optional(),
magicNoteCommentFormat: magicNoteCommentFormatSchema.optional(),
+13
View File
@@ -2191,6 +2191,19 @@ export type DesktopApi = {
entries: AssistantHeartbeatEntry[]
}>
}
supervision: {
overview: (input?: { target?: import('./supervision-contracts').SupervisionTarget }) => Promise<import('./supervision-contracts').SupervisionResultView[]>
run: (input: unknown) => Promise<unknown>
graph: (input?: import('./supervision-contracts').SupervisionGraphRequest) => Promise<Record<string, unknown>>
source: (sourceId: string) => Promise<Record<string, unknown> | undefined>
sourceContext: (sourceId: string) => Promise<Record<string, unknown>>
continueContext: (input: import('./supervision-contracts').SupervisionContinueContextRequest) => Promise<Record<string, unknown>>
continue: (input: import('./supervision-contracts').SupervisionContinueRequest) => Promise<void>
knowledgePreview: (input: import('./supervision-contracts').SupervisionKnowledgePreviewRequest) => Promise<Record<string, unknown>>
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>
}
experts: {
list: () => Promise<AssistantExpert[]>
create: (input: ExpertCreateInput) => Promise<AssistantExpert>
+11
View File
@@ -264,6 +264,17 @@ export const ipcChannels = {
heartbeatsRemove: 'heartbeats:remove',
heartbeatsRunNow: 'heartbeats:run-now',
heartbeatsHistory: 'heartbeats:history',
supervisionOverview: 'supervision:overview',
supervisionRun: 'supervision:run',
supervisionGraph: 'supervision:graph',
supervisionSource: 'supervision:source',
supervisionEntityAction: 'supervision:entity-action',
supervisionRelationAction: 'supervision:relation-action',
supervisionSourceContext: 'supervision:source-context',
supervisionContinueContext: 'supervision:continue-context',
supervisionContinue: 'supervision:continue',
supervisionKnowledgePreview: 'supervision:knowledge-preview',
supervisionKnowledgeCommit: 'supervision:knowledge-commit',
expertsList: 'experts:list',
expertsCreate: 'experts:create',
expertsUpdate: 'experts:update',
+186
View File
@@ -0,0 +1,186 @@
import { z } from 'zod'
import { heartbeatScopeSchema } from './assistant-contracts'
export const supervisionTriggerSchema = z.enum(['manual', 'heartbeat'])
export type SupervisionTrigger = z.infer<typeof supervisionTriggerSchema>
export const supervisionTimeRangeSchema = z
.object({
from: z.string().datetime({ offset: true }),
to: z.string().datetime({ offset: true })
})
.strict()
.refine((value) => Date.parse(value.from) <= Date.parse(value.to), {
message: '监督者时间范围无效'
})
export const supervisionRunRequestSchema = z
.object({
trigger: supervisionTriggerSchema,
scope: heartbeatScopeSchema,
timeRange: supervisionTimeRangeSchema
})
.strict()
export type SupervisionRunRequest = z.infer<typeof supervisionRunRequestSchema>
export const supervisionEvidenceSchema = z
.object({
id: z.string().min(1).max(256),
sourceType: z.enum(['conversation', 'task', 'note', 'memory', 'knowledge']),
sourceId: z.string().min(1).max(256),
title: z.string().max(500),
content: z.string().max(8_000),
occurredAt: z.string().datetime({ offset: true }),
locator: z.record(z.string(), z.unknown()).optional()
})
.strict()
export type SupervisionEvidence = z.infer<typeof supervisionEvidenceSchema>
const sourceReferenceIdsSchema = z.array(z.string().min(1).max(256)).max(20)
export const supervisionEventSchema = z
.object({
title: z.string().min(1).max(240),
description: z.string().max(2_000),
occurredAt: z.string().datetime({ offset: true }),
eventType: z.enum(['decision', 'change', 'discussion', 'milestone']),
entityIds: z.array(z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/)).max(40),
sourceReferenceIds: sourceReferenceIdsSchema
})
.strict()
export const supervisionEntitySchema = z
.object({
id: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
persistedId: z.string().uuid().optional(),
label: z.string().min(1).max(240),
description: z.string().max(2_000),
sourceReferenceIds: sourceReferenceIdsSchema
})
.strict()
export const supervisionRelationSchema = z
.object({
fromEntityId: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
toEntityId: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
relationType: z.enum(['supports', 'depends-on', 'contrasts', 'related']),
reason: z.string().max(1_000),
sourceReferenceIds: sourceReferenceIdsSchema
})
.strict()
export const supervisionEntityChangeSchema = z.object({
entityId: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
changeType: z.enum(['proposed', 'added', 'verified', 'revised']),
description: z.string().max(2_000),
sourceReferenceIds: sourceReferenceIdsSchema
}).strict()
export const supervisionSummaryOutputSchema = z
.object({
summary: z.string().min(1).max(12_000),
changeDigest: z.string().max(4_000),
openItems: z.array(z.string().min(1).max(500)).max(20),
events: z.array(supervisionEventSchema).max(40),
entities: z.array(supervisionEntitySchema).max(40),
entityChanges: z.array(supervisionEntityChangeSchema).max(80),
relations: z.array(supervisionRelationSchema).max(80)
})
.strict()
export type SupervisionSummaryOutput = z.infer<
typeof supervisionSummaryOutputSchema
>
export const supervisionEntityActionSchema = z
.object({
resultId: z.string().min(1).optional(),
entityId: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
action: z.enum(['confirm', 'revise', 'revoke']),
label: z.string().trim().min(1).max(240).optional(),
description: z.string().max(2_000).optional()
})
.strict()
export const supervisionRelationActionSchema = z
.object({
resultId: z.string().min(1).optional(),
relationId: z.string().uuid(),
action: z.enum(['confirm', 'revoke'])
})
.strict()
export const supervisionSourceRequestSchema = z
.object({ sourceId: z.string().min(1).max(256) })
.strict()
export const supervisionContinueContextRequestSchema = z.object({
sourceId: z.string().min(1).max(256),
resultId: z.string().min(1).max(256)
}).strict()
export type SupervisionContinueContextRequest = z.infer<typeof supervisionContinueContextRequestSchema>
export const supervisionContinueRequestSchema = z.object({
conversationId: z.string().min(1).max(128),
prompt: z.string().trim().min(1).max(12_000),
runtimeSelection: z.unknown().optional(),
projectId: z.string().min(1).max(128).optional()
}).strict()
export type SupervisionContinueRequest = z.infer<typeof supervisionContinueRequestSchema>
export const supervisionKnowledgePreviewRequestSchema = z.object({
operation: z.enum(['create-entity', 'update-entity']),
libraryId: z.string().uuid(),
entityId: z.string().uuid().optional(),
label: z.string().trim().min(1).max(120),
type: z.string().trim().min(1).max(120),
description: z.string().trim().max(2_000),
aliases: z.array(z.string().trim().min(1).max(120)).max(50),
sourceId: z.string().min(1).max(256)
}).strict().superRefine((value, context) => {
if (value.operation === 'update-entity' && !value.entityId) {
context.addIssue({ code: 'custom', path: ['entityId'], message: 'update-entity requires entityId' })
}
})
export type SupervisionKnowledgePreviewRequest = z.infer<typeof supervisionKnowledgePreviewRequestSchema>
export const supervisionKnowledgeCommitRequestSchema = z.object({
previewId: z.string().uuid()
}).strict()
export type SupervisionKnowledgeCommitRequest = z.infer<typeof supervisionKnowledgeCommitRequestSchema>
export type SupervisionEntityAction = z.infer<typeof supervisionEntityActionSchema>
export type SupervisionRelationAction = z.infer<typeof supervisionRelationActionSchema>
export const supervisionResultViewSchema = z.object({
storyLineId: z.string(),
sourceId: z.string().nullable(),
id: z.string(), summary: z.string(), changeDigest: z.string(), createdAt: z.string(),
scope: heartbeatScopeSchema, timeRange: supervisionTimeRangeSchema,
openItems: z.array(z.string())
})
export type SupervisionResultView = z.infer<typeof supervisionResultViewSchema>
export const supervisionTargetSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('conversation'), conversationId: z.string().min(1).max(128) }).strict(),
z.object({ type: z.literal('task'), taskId: z.string().min(1).max(128) }).strict()
])
export type SupervisionTarget = z.infer<typeof supervisionTargetSchema>
export const supervisionOverviewRequestSchema = z.object({ target: supervisionTargetSchema.optional() }).strict()
export const supervisionGraphRequestSchema = z.object({
resultId: z.string().min(1).optional(), storyLineId: z.string().min(1).optional()
}).strict()
export type SupervisionGraphRequest = z.infer<typeof supervisionGraphRequestSchema>
export const supervisionGraphViewSchema = z.object({
storyLine: z.object({ id: z.string(), scope_json: z.string() }).nullable(),
events: z.array(z.object({ id: z.string(), title: z.string(), description: z.string(), occurred_at: z.string() })),
entities: z.array(z.object({ id: z.string(), canonical_label: z.string(), description: z.string(), confirmation_state: z.string() })),
relations: z.array(z.object({ id: z.string(), from_entity_id: z.string(), to_entity_id: z.string(), relation_type: z.string(), reason: z.string(), confirmation_state: z.string() })),
sources: z.array(z.object({ id: z.string(), title: z.string(), occurred_at: z.string() })),
eventEntities: z.array(z.object({ event_id: z.string(), entity_id: z.string() })),
eventSources: z.array(z.object({ event_id: z.string(), source_id: z.string() }))
})
export type SupervisionGraphView = z.infer<typeof supervisionGraphViewSchema>
+7
View File
@@ -13,6 +13,13 @@ const terminalId = '00000000-0000-4000-8000-000000000102'
const projectId = '00000000-0000-4000-8000-000000000201'
describe('workbar contracts', () => {
it('persists supervision conversation and task targets without allowing task terminal bindings', () => {
for (const targetRef of [{ type: 'conversation', conversationId: 'A' }, { type: 'task', taskId: 'task-A' }]) {
const instance = { id: taskId, appId: 'tasks', title: 'Tasks', targetRef }
expect(workbarTabInstanceSchema.parse(instance).targetRef).toEqual(targetRef)
expect(workbarTabInstanceSchema.safeParse({ ...instance, appId: 'terminal' }).success).toBe(false)
}
})
it('freezes the first application catalog and instance policies', () => {
expect(
WORKBAR_APP_DEFINITIONS.map(({ id, instancePolicy, defaultContext, defaultOpen, required, closable, reorderable }) => ({
+7 -2
View File
@@ -183,6 +183,7 @@ export type WorkbarExecutionTargetRef = z.infer<
export const workbarTargetRefSchema = z.union([
workbarExecutionTargetRefSchema,
z.object({ type: z.literal('task'), taskId: z.string().min(1).max(128) }).strict(),
z
.object({
type: z.literal('conversation'),
@@ -206,7 +207,7 @@ export const workbarTabInstanceSchema = z
.superRefine((instance, context) => {
if (
instance.appId === 'terminal' &&
(!instance.targetRef || instance.targetRef.type === 'conversation')
(!instance.targetRef || !['local', 'project'].includes(instance.targetRef.type))
) {
context.addIssue({
code: 'custom',
@@ -227,14 +228,18 @@ export const workbarTabInstanceSchema = z
}
if (
instance.appId !== 'browser' &&
instance.appId !== 'tasks' &&
instance.targetRef?.type === 'conversation'
) {
context.addIssue({
code: 'custom',
path: ['targetRef'],
message: 'Conversation bindings are reserved for browser instances'
message: 'Conversation bindings require browser or tasks instances'
})
}
if (instance.targetRef?.type === 'task' && instance.appId !== 'tasks') {
context.addIssue({ code: 'custom', path: ['targetRef'], message: 'Task bindings require tasks instances' })
}
})
export type WorkbarTabInstance = z.infer<
typeof workbarTabInstanceSchema