From 06f272316106cf3c9b737fee0ceec52d98268df3 Mon Sep 17 00:00:00 2001 From: weichen Date: Tue, 13 Oct 2026 01:04:56 +0000 Subject: [PATCH] Add persistent work snapshots and checkpoint contracts Why: preserve scoped work findings and truthful activity across reloads. Evidence: task JSON loss and validation/state regressions RED to GREEN; complete Core suite. Impact: optional card/document wire fields and bounded candidate validation; old Body remains supported. Refs: fast-slow U2/U3 Task1, R03 R05 R09 R17 --- activity.go | 27 ++--- data.go | 93 +++++++-------- task_snapshot.go | 258 ++++++++++++++++++++++++++++++++++++++++++ task_snapshot_test.go | 99 ++++++++++++++++ ts/types.ts | 24 +++- 5 files changed, 441 insertions(+), 60 deletions(-) create mode 100644 task_snapshot.go create mode 100644 task_snapshot_test.go diff --git a/activity.go b/activity.go index 2c2106a..dec01ff 100644 --- a/activity.go +++ b/activity.go @@ -12,19 +12,20 @@ import "time" type ActivityCard struct { ID ID `json:"id"` // ProjectID/ConversationID 定位归属;ConversationID 为空表示项目级卡片(远期跨会话收件箱)。 - ProjectID ID `json:"projectId"` - ConversationID ID `json:"conversationId,omitempty"` - SourceRunID ID `json:"sourceRunId,omitempty"` // 溯源:产生卡片的 run - AnchorMessageID ID `json:"anchorMessageId,omitempty"` // 互定位:出生消息(↙ 定位到对话) - Kind CardKind `json:"kind"` - Title string `json:"title"` - Status CardStatus `json:"status"` - Tone string `json:"tone,omitempty"` // ok | warn | danger | info - Body []UINode `json:"body,omitempty"` - Actions []UIAction `json:"actions,omitempty"` // 走命令总线;未绑定 command 降级为发消息 - Pinned bool `json:"pinned,omitempty"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ProjectID ID `json:"projectId"` + ConversationID ID `json:"conversationId,omitempty"` + SourceRunID ID `json:"sourceRunId,omitempty"` // 溯源:产生卡片的 run + AnchorMessageID ID `json:"anchorMessageId,omitempty"` // 互定位:出生消息(↙ 定位到对话) + Kind CardKind `json:"kind"` + Title string `json:"title"` + Status CardStatus `json:"status"` + Tone string `json:"tone,omitempty"` // ok | warn | danger | info + Task *TaskSnapshot `json:"task,omitempty"` + Body []UINode `json:"body,omitempty"` + Actions []UIAction `json:"actions,omitempty"` // 走命令总线;未绑定 command 降级为发消息 + Pinned bool `json:"pinned,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type CardKind string diff --git a/data.go b/data.go index 58d701c..5d1d4f8 100644 --- a/data.go +++ b/data.go @@ -13,18 +13,19 @@ type ParsedDocument struct { } type DocMeta struct { - Filename string `json:"filename"` - MIME string `json:"mime,omitempty"` - Parser string `json:"parser"` // builtin:md / builtin:docx / mineru / agent - Confidence float64 `json:"confidence,omitempty"` // 0–1;交叉验证差异块拉低 - CrossCheck *CrossCheck `json:"crossCheck,omitempty"` - ParsedAt time.Time `json:"parsedAt"` + Origin *DocumentOrigin `json:"origin,omitempty"` + Filename string `json:"filename"` + MIME string `json:"mime,omitempty"` + Parser string `json:"parser"` // builtin:md / builtin:docx / mineru / agent + Confidence float64 `json:"confidence,omitempty"` // 0–1;交叉验证差异块拉低 + CrossCheck *CrossCheck `json:"crossCheck,omitempty"` + ParsedAt time.Time `json:"parsedAt"` } type CrossCheck struct { - With string `json:"with"` // 交叉验证的另一解析器 - DiffBlocks int `json:"diffBlocks"` - Verdict string `json:"verdict"` // agree | differ + With string `json:"with"` // 交叉验证的另一解析器 + DiffBlocks int `json:"diffBlocks"` + Verdict string `json:"verdict"` // agree | differ } type DocBlockType string @@ -39,13 +40,13 @@ const ( ) type DocBlock struct { - Type DocBlockType `json:"type"` - Level int `json:"level,omitempty"` // heading - Text string `json:"text,omitempty"` // para/heading/list 项拼接/code - Items []string `json:"items,omitempty"` // list - Rows [][]string `json:"rows,omitempty"` // table(首行为表头) - Ref string `json:"ref,omitempty"` // image-ref 的对象键 - Confidence float64 `json:"confidence,omitempty"` + Type DocBlockType `json:"type"` + Level int `json:"level,omitempty"` // heading + Text string `json:"text,omitempty"` // para/heading/list 项拼接/code + Items []string `json:"items,omitempty"` // list + Rows [][]string `json:"rows,omitempty"` // table(首行为表头) + Ref string `json:"ref,omitempty"` // image-ref 的对象键 + Confidence float64 `json:"confidence,omitempty"` } // PlainText 把文档压成纯文本(附件注入与全文索引用);表格按行拼接。 @@ -88,60 +89,60 @@ func (d ParsedDocument) PlainText() string { // ParserDesc 注册表条目(来源标记:builtin/plugin/...;一切可禁用)。 type ParserDesc struct { - ID string `json:"id"` - Formats []string `json:"formats"` // 支持的扩展名 + ID string `json:"id"` + Formats []string `json:"formats"` // 支持的扩展名 Source SourceKind `json:"source"` - Note string `json:"note,omitempty"` + Note string `json:"note,omitempty"` } // ---- 检索(4.12)---- // KnowledgeDoc 是检索语料的一条文档(来自 ingest 或插件知识库,带来源标记)。 type KnowledgeDoc struct { - ID ID `json:"id"` - Owner ID `json:"owner,omitempty"` // 空 = 共享(插件级)知识 - ProjectID ID `json:"projectId,omitempty"` // 空 = 插件级知识库 - Source string `json:"source"` // upload | artifact | plugin: - Title string `json:"title"` - Ref string `json:"ref,omitempty"` // 工件/文件引用 - Chunk string `json:"chunk"` + ID ID `json:"id"` + Owner ID `json:"owner,omitempty"` // 空 = 共享(插件级)知识 + ProjectID ID `json:"projectId,omitempty"` // 空 = 插件级知识库 + Source string `json:"source"` // upload | artifact | plugin: + Title string `json:"title"` + Ref string `json:"ref,omitempty"` // 工件/文件引用 + Chunk string `json:"chunk"` CreatedAt time.Time `json:"createdAt"` } type RetrievalHit struct { - Doc KnowledgeDoc `json:"doc"` - Score float64 `json:"score"` - Kind string `json:"kind"` // fulltext | vector | plugin + Doc KnowledgeDoc `json:"doc"` + Score float64 `json:"score"` + Kind string `json:"kind"` // fulltext | vector | plugin } type RetrievalQuery struct { - ProjectID ID `json:"projectId,omitempty"` - Text string `json:"text"` - TopK int `json:"topK,omitempty"` + ProjectID ID `json:"projectId,omitempty"` + Text string `json:"text"` + TopK int `json:"topK,omitempty"` Sources []string `json:"sources,omitempty"` // 过滤来源 } // ---- 记忆(4.13 契约;实现远期)---- type MemoryEntry struct { - ID string `json:"id"` - Scope string `json:"scope"` // user | project - ProjectID ID `json:"projectId,omitempty"` - Type string `json:"type"` // fact | preference | decision | episode | procedure - Content string `json:"content"` - Source string `json:"source"` // run: | manual - Confidence float64 `json:"confidence,omitempty"` - CreatedAt time.Time `json:"createdAt"` - LastAccessed time.Time `json:"lastAccessed"` - ExpiresAt *time.Time `json:"expiresAt,omitempty"` + ID string `json:"id"` + Scope string `json:"scope"` // user | project + ProjectID ID `json:"projectId,omitempty"` + Type string `json:"type"` // fact | preference | decision | episode | procedure + Content string `json:"content"` + Source string `json:"source"` // run: | manual + Confidence float64 `json:"confidence,omitempty"` + CreatedAt time.Time `json:"createdAt"` + LastAccessed time.Time `json:"lastAccessed"` + ExpiresAt *time.Time `json:"expiresAt,omitempty"` } // ---- 上传文件 ---- type UploadInfo struct { - ArtifactID ID `json:"artifactId"` - Filename string `json:"filename"` - MIME string `json:"mime,omitempty"` - Size int64 `json:"size"` + ArtifactID ID `json:"artifactId"` + Filename string `json:"filename"` + MIME string `json:"mime,omitempty"` + Size int64 `json:"size"` Parsed *ParsedDocument `json:"parsed,omitempty"` } diff --git a/task_snapshot.go b/task_snapshot.go new file mode 100644 index 0000000..9db62df --- /dev/null +++ b/task_snapshot.go @@ -0,0 +1,258 @@ +package core + +import ( + "encoding/json" + "fmt" + "strings" + "time" +) + +type DocumentOrigin struct { + ArtifactID ID `json:"artifactId"` + ContentHash string `json:"contentHash"` + ParserRevision string `json:"parserRevision"` +} +type TaskPhase struct { + ID string `json:"id"` + Label string `json:"label"` + ObservedAt time.Time `json:"observedAt,omitempty"` +} +type TaskSourceRef struct { + ID string `json:"id"` + ArtifactID string `json:"artifactId"` + ContentHash string `json:"contentHash"` + ParserRevision string `json:"parserRevision"` + BlockIndex int `json:"blockIndex"` + Label string `json:"label"` + Quote string `json:"quote,omitempty"` + Availability string `json:"availability,omitempty"` +} +type TaskResultRef struct { + ArtifactID ID `json:"artifactId,omitempty"` + SurfaceID ID `json:"surfaceId,omitempty"` + Version string `json:"version,omitempty"` + ContentHash string `json:"contentHash,omitempty"` + Availability string `json:"availability"` + Name string `json:"name,omitempty"` +} +type TaskFinding struct { + ID string `json:"id"` + Text string `json:"text"` + Impact string `json:"impact,omitempty"` + Kind string `json:"kind"` + Status string `json:"status"` + Revision int64 `json:"revision"` + Sources []TaskSourceRef `json:"sources"` + Assumptions string `json:"assumptions,omitempty"` + Corrects string `json:"corrects,omitempty"` +} +type TaskSnapshot struct { + SchemaVersion int `json:"schemaVersion"` + TaskID ID `json:"taskId"` + AttemptID ID `json:"attemptId"` + CardID ID `json:"cardId"` + ProjectID ID `json:"projectId"` + ConversationID ID `json:"conversationId"` + SourceMessageID ID `json:"sourceMessageId,omitempty"` + ParentRunID ID `json:"parentRunId,omitempty"` + RunID ID `json:"runId,omitempty"` + OwnerInstanceID string `json:"ownerInstanceId"` + ExecutionEpoch int64 `json:"executionEpoch,omitempty"` + Revision int64 `json:"revision"` + SourceSeq int64 `json:"sourceSeq"` + StreamScope string `json:"streamScope"` + InputRevision string `json:"inputRevision,omitempty"` + Title string `json:"title"` + Goal string `json:"goal,omitempty"` + ExecutionState string `json:"executionState"` + OutcomeCompleteness string `json:"outcomeCompleteness"` + DeliveryState string `json:"deliveryState"` + Phase TaskPhase `json:"phase"` + Focus string `json:"focus,omitempty"` + Coverage string `json:"coverage,omitempty"` + Remaining string `json:"remaining,omitempty"` + NextStep string `json:"nextStep,omitempty"` + Conclusion string `json:"conclusion,omitempty"` + Findings []TaskFinding `json:"findings,omitempty"` + Actions []UIAction `json:"actions,omitempty"` + ResultRefs []TaskResultRef `json:"resultRefs,omitempty"` + LastActivityAt time.Time `json:"lastActivityAt,omitempty"` + LastMeaningfulUpdateAt time.Time `json:"lastMeaningfulUpdateAt,omitempty"` + CheckpointKeys map[string]string `json:"checkpointKeys,omitempty"` + CheckpointCount int `json:"checkpointCount,omitempty"` + CheckpointSeq int64 `json:"checkpointSeq,omitempty"` +} +type TaskSourceQuote struct { + SourceID string `json:"sourceId"` + BlockIndex int `json:"blockIndex"` + Quote string `json:"quote"` +} +type WorkCheckpointCandidate struct { + CheckpointID string `json:"checkpointId"` + PhaseID string `json:"phaseId"` + Focus string `json:"focus,omitempty"` + Remaining string `json:"remaining,omitempty"` + NextStep string `json:"nextStep,omitempty"` + Findings []WorkFindingCandidate `json:"findings"` +} +type WorkFindingCandidate struct { + FindingKey string `json:"findingKey"` + Text string `json:"text"` + Impact string `json:"impact,omitempty"` + Kind string `json:"kind"` + Assumptions string `json:"assumptions,omitempty"` + Corrects string `json:"corrects,omitempty"` + Sources []TaskSourceQuote `json:"sources"` +} + +// Validate enforces wire limits; owner/source authorization belongs to Backend. +func (s TaskSnapshot) Validate() error { + if s.SchemaVersion != 1 { + return fmt.Errorf("task.schema: unsupported") + } + if s.TaskID == "" || s.AttemptID == "" || s.CardID == "" || s.ProjectID == "" || s.ConversationID == "" || s.OwnerInstanceID == "" { + return fmt.Errorf("task.scope: missing identity") + } + if s.StreamScope != "conversation:"+s.ConversationID { + return fmt.Errorf("task.scope: inconsistent stream") + } + if s.Revision < 1 || s.Revision > 9007199254740991 || s.SourceSeq < 0 || s.SourceSeq > 9007199254740991 || s.ExecutionEpoch < 0 { + return fmt.Errorf("task.revision: unsafe cursor") + } + switch s.ExecutionState { + case "queued", "running", "awaiting_user", "completed", "failed", "interrupted": + default: + return fmt.Errorf("task.state: unknown") + } + if (s.ExecutionState == "running" || s.ExecutionState == "awaiting_user") && s.RunID == "" { + return fmt.Errorf("task.run: active task requires run") + } + switch s.OutcomeCompleteness { + case "none", "partial", "complete": + default: + return fmt.Errorf("task.outcome: unknown") + } + switch s.DeliveryState { + case "pending", "available", "reported": + default: + return fmt.Errorf("task.delivery: unknown") + } + active := 0 + for _, f := range s.Findings { + if f.ID == "" || f.Revision < 1 { + return fmt.Errorf("task.finding: missing identity") + } + switch f.Kind { + case "material_record", "analysis", "provisional", "needs_user", "verified": + default: + return fmt.Errorf("task.finding.kind: unknown") + } + switch f.Status { + case "active": + active++ + case "corrected", "withdrawn", "superseded": + default: + return fmt.Errorf("task.finding.status: unknown") + } + } + if active > 12 { + return fmt.Errorf("task.findings: too many active") + } + b, err := json.Marshal(s) + if err != nil { + return err + } + if len(b) > 64*1024 { + return fmt.Errorf("task.size: exceeds 64KiB") + } + return nil +} +func (s TaskSnapshot) Terminal() bool { + return s.ExecutionState == "completed" || s.ExecutionState == "failed" || s.ExecutionState == "interrupted" +} +func ValidateWorkCheckpoint(c WorkCheckpointCandidate) error { + if strings.TrimSpace(c.CheckpointID) == "" || len(c.CheckpointID) > 128 { + return fmt.Errorf("checkpoint.id: required or too long") + } + switch c.PhaseID { + case "reading", "crosscheck", "summarizing", "draft-ready": + default: + return fmt.Errorf("checkpoint.phase: unknown") + } + if len(c.Findings) == 0 || len(c.Findings) > 6 { + return fmt.Errorf("checkpoint.findings: require 1..6") + } + keys := map[string]bool{} + for _, f := range c.Findings { + switch f.Kind { + case "material_record", "analysis", "provisional", "needs_user": + default: + return fmt.Errorf("checkpoint.kind: model cannot grant verified status") + } + if f.FindingKey == "" || keys[f.FindingKey] || strings.TrimSpace(f.Text) == "" { + return fmt.Errorf("checkpoint.finding: empty or duplicate key") + } + keys[f.FindingKey] = true + if len(f.Sources) == 0 { + return fmt.Errorf("checkpoint.sources: evidence required") + } + for _, src := range f.Sources { + if src.SourceID == "" || src.BlockIndex < 0 || strings.TrimSpace(src.Quote) == "" { + return fmt.Errorf("checkpoint.source: invalid") + } + } + if (f.Kind == "analysis" || f.Kind == "provisional") && strings.TrimSpace(f.Assumptions) == "" { + return fmt.Errorf("checkpoint.assumptions: required") + } + } + b, err := json.Marshal(c) + if err != nil { + return err + } + if len(b) > 16*1024 { + return fmt.Errorf("checkpoint.size: exceeds 16KiB") + } + return nil +} +func (s TaskSnapshot) SummaryBody() []UINode { + labels := map[string]string{"queued": "排队中", "running": "进行中", "awaiting_user": "需要你处理", "completed": "已完成", "failed": "执行失败", "interrupted": "已中断"} + var lines []string + for _, f := range s.Findings { + if f.Status == "active" { + line := f.Text + if f.Impact != "" { + line += ";" + f.Impact + } + lines = append(lines, line) + } + } + note := strings.Join(lines, "\n") + if note == "" { + note = TaskSummaryNoFindingsYet + if s.ExecutionState == "queued" { + note = "尚未开始处理,等待执行资源" + } + if s.Terminal() { + note = s.Conclusion + if note == "" { + note = "没有可用阶段成果" + } + } + } + v := TaskSummaryView{Focus: s.Focus, FindingsNote: note, Remaining: s.Remaining, NextStep: s.NextStep, OutcomeLine: s.Conclusion} + var body []UINode + if s.Terminal() { + body = v.FinishedBody(labels[s.ExecutionState], nil) + } else { + body = v.RunningBody(labels[s.ExecutionState], nil) + } + if s.Coverage != "" { + body = append(body, UINode{Type: UIKV, KV: [][2]string{{"提供范围", s.Coverage}}}) + } + for _, r := range s.ResultRefs { + if r.ArtifactID != "" && r.Availability == "available" { + body = append(body, UINode{Type: UIDocRef, DocID: r.ArtifactID, Title: r.Name}) + } + } + return body +} diff --git a/task_snapshot_test.go b/task_snapshot_test.go new file mode 100644 index 0000000..309d4ba --- /dev/null +++ b/task_snapshot_test.go @@ -0,0 +1,99 @@ +package core + +import ( + "encoding/json" + "strings" + "testing" +) + +// Loss of the optional task object makes refresh discard stage findings. +func TestTaskSnapshotCardRoundTrip(t *testing.T) { + raw := []byte(`{"id":"card1","projectId":"p1","conversationId":"c1","kind":"progress","status":"active","task":{"schemaVersion":1,"taskId":"t1","attemptId":"a1","cardId":"card1","revision":4,"executionState":"running","focus":"核对材料","findings":[{"id":"f1","text":"日期不同","kind":"material_record","status":"active","revision":1}]}}`) + var card ActivityCard + if err := json.Unmarshal(raw, &card); err != nil { + t.Fatal(err) + } + encoded, err := json.Marshal(card) + if err != nil { + t.Fatal(err) + } + var restored map[string]json.RawMessage + if err := json.Unmarshal(encoded, &restored); err != nil { + t.Fatal(err) + } + if _, ok := restored["task"]; !ok { + t.Fatal("task findings were lost in card persistence") + } + var task map[string]json.RawMessage + if err := json.Unmarshal(restored["task"], &task); err != nil { + t.Fatal(err) + } + if string(task["revision"]) != "4" || string(task["focus"]) != `"核对材料"` { + t.Fatal("revision/focus did not survive") + } +} + +func validTaskSnapshot() TaskSnapshot { + return TaskSnapshot{SchemaVersion: 1, TaskID: "t1", AttemptID: "a1", CardID: "card1", ProjectID: "p1", ConversationID: "c1", OwnerInstanceID: "i1", Revision: 1, ExecutionState: "queued", OutcomeCompleteness: "none", DeliveryState: "pending", StreamScope: "conversation:c1", Title: "整理材料"} +} +func TestTaskSnapshotRejectsUnsafeAndUnboundState(t *testing.T) { + for _, mutate := range []func(*TaskSnapshot){ + func(s *TaskSnapshot) { s.TaskID = "" }, func(s *TaskSnapshot) { s.AttemptID = "" }, + func(s *TaskSnapshot) { s.Revision = 9007199254740992 }, func(s *TaskSnapshot) { s.SourceSeq = -1 }, + func(s *TaskSnapshot) { s.ExecutionState = "imagined" }, func(s *TaskSnapshot) { s.StreamScope = "conversation:other" }, + func(s *TaskSnapshot) { s.ExecutionState = "running" }, + } { + s := validTaskSnapshot() + mutate(&s) + if s.Validate() == nil { + t.Errorf("invalid snapshot accepted: %+v", s) + } + } + if err := validTaskSnapshot().Validate(); err != nil { + t.Fatal(err) + } +} +func TestWorkCheckpointRejectsVerifiedAndEmptyEvidence(t *testing.T) { + c := WorkCheckpointCandidate{CheckpointID: "cp1", PhaseID: "crosscheck", Findings: []WorkFindingCandidate{{FindingKey: "date", Text: "日期不同", Kind: "verified", Sources: []TaskSourceQuote{{SourceID: "src1", Quote: "六月"}}}}} + if err := ValidateWorkCheckpoint(c); err == nil || !strings.Contains(err.Error(), "kind") { + t.Fatal("model granted verified status", err) + } + c.Findings[0].Kind = "material_record" + c.Findings[0].Sources = nil + if ValidateWorkCheckpoint(c) == nil { + t.Fatal("source-free fact accepted") + } + c.Findings[0].Sources = []TaskSourceQuote{{SourceID: "src1", Quote: "六月"}} + if err := ValidateWorkCheckpoint(c); err != nil { + t.Fatal(err) + } + c.Findings[0].Text = strings.Repeat("x", 17000) + if ValidateWorkCheckpoint(c) == nil { + t.Fatal("unbounded candidate accepted") + } +} +func TestTaskSnapshotBodyPreservesLimitsAndHonestStates(t *testing.T) { + s := validTaskSnapshot() + s.Remaining = "图片内容未纳入" + s.ExecutionState = "failed" + s.Conclusion = "处理失败,已有发现可查看" + s.Findings = []TaskFinding{{ID: "f1", Text: "金额缺失", Kind: "material_record", Status: "active", Revision: 1}} + raw, _ := json.Marshal(s.SummaryBody()) + for _, want := range []string{"执行失败", "图片内容未纳入", "金额缺失", "处理失败"} { + if !strings.Contains(string(raw), want) { + t.Errorf("lost critical summary %s", want) + } + } +} + +func TestTaskSnapshotUnstartedAndFinishedDoNotClaimReading(t *testing.T) { + for _, state := range []string{"queued", "completed", "interrupted"} { + s := validTaskSnapshot() + s.ExecutionState = state + s.Conclusion = "处理已结束" + b, _ := json.Marshal(s.SummaryBody()) + if strings.Contains(string(b), "正在读取") { + t.Errorf("%s falsely claims active reading", state) + } + } +} diff --git a/ts/types.ts b/ts/types.ts index dd4ecd6..1dece06 100644 --- a/ts/types.ts +++ b/ts/types.ts @@ -149,6 +149,7 @@ export interface ActivityCard { id: ID; projectId: ID; conversationId?: ID; sourceRunId?: ID; anchorMessageId?: ID; kind: CardKind; title: string; status: CardStatus; tone?: string; + task?: TaskSnapshot; body?: UINode[]; actions?: { id: string; label: string; style?: 'primary' | 'default' | 'danger'; command?: string }[]; pinned?: boolean; createdAt: string; updatedAt: string; } @@ -334,7 +335,7 @@ export interface PluginPageDecl { export interface ParsedDocument { meta: { - filename: string; mime?: string; parser: string; confidence?: number; + filename: string; mime?: string; parser: string; confidence?: number; origin?: DocumentOrigin; crossCheck?: { with: string; diffBlocks: number; verdict: 'agree' | 'differ' }; parsedAt: string; }; @@ -397,3 +398,24 @@ export interface SurfaceMeta { conversationId?: ID; updatedAt: string; } + +// Persistent work projection; host-owned identities and revisions. +export interface DocumentOrigin { artifactId: ID; contentHash: string; parserRevision: string } +export interface TaskPhase { id: string; label: string; observedAt?: string } +export interface TaskSourceRef { id: string; artifactId: ID; contentHash: string; parserRevision: string; blockIndex: number; label: string; quote?: string; availability?: string } +export interface TaskResultRef { artifactId?: ID; surfaceId?: ID; version?: string; contentHash?: string; availability: string; name?: string } +export interface TaskFinding { id: string; text: string; impact?: string; kind: string; status: string; revision: number; sources: TaskSourceRef[]; assumptions?: string; corrects?: string } +export interface TaskSnapshot { + schemaVersion: number; taskId: ID; attemptId: ID; cardId: ID; projectId: ID; conversationId: ID; + sourceMessageId?: ID; parentRunId?: ID; runId?: ID; ownerInstanceId: string; executionEpoch?: number; + revision: number; sourceSeq: number; streamScope: string; inputRevision?: string; title: string; goal?: string; + executionState: 'queued'|'running'|'awaiting_user'|'completed'|'failed'|'interrupted'; outcomeCompleteness: 'none'|'partial'|'complete'; deliveryState: 'pending'|'available'|'reported'; phase: TaskPhase; + focus?: string; coverage?: string; remaining?: string; nextStep?: string; conclusion?: string; + findings?: TaskFinding[]; actions?: UIAction[]; resultRefs?: TaskResultRef[]; lastActivityAt?: string; lastMeaningfulUpdateAt?: string; + checkpointKeys?: Record; checkpointCount?: number; checkpointSeq?: number; +} +export interface TaskSourceQuote { sourceId: string; blockIndex: number; quote: string } +export interface WorkFindingCandidate { findingKey: string; text: string; impact?: string; kind: string; assumptions?: string; corrects?: string; sources: TaskSourceQuote[] } +export interface WorkCheckpointCandidate { checkpointId: string; phaseId: string; focus?: string; remaining?: string; nextStep?: string; findings: WorkFindingCandidate[] } + +export interface UIAction { id: string; label: string; style?: string; command?: string }