feat(digest): related_tasks 只读折叠(§2 跨任务)
关联/继承任务的概览现在复用其【自身的折叠视图】,与当前任务同逻辑: - relatedTaskOverviews 按 source 任务自己的 covered/hot 折出已折成员(recent_facts / recent_intent_results 跳过已折且仍冷的成员),并把该任务的 cold_digests(body)只读带出, 统一 inherited=true / source_task_id 标记。 - expand_digest 支持跨任务:先查本任务,再查直接关联任务(read-only),返回成员紧凑列表并 标继承;node_detail 已有的 GetNodeWithSources 负责成员完整还原。 - 复活检查同样生效:source 任务里已复活(转热)的被覆盖成员照常显示,不被折出。 至此设计文档 §2 跨任务折叠全部落地;仅 §2.3 墙钟兜底(任务停摆判稳)按既定作为后续。
This commit is contained in:
1 parent
eefd5ee2f2
commit
775189d916
2 files changed
+98
-11
No files matched your search
+23
-1
@@ -810,6 +810,9 @@ func (t *ToolSet) relatedTaskOverviews() []map[string]any {
|
||||
out := make([]map[string]any, 0, len(sources))
|
||||
for _, source := range sources {
|
||||
ts := source.Store
|
||||
// §2 cross-task: render the source task's OWN folded view — fold out the
|
||||
// members it has already folded, and surface its cold_digests read-only.
|
||||
hidden := hiddenMembersFor(ts)
|
||||
budget := overviewTextBudget{remaining: perSourceTextBudget}
|
||||
item := map[string]any{
|
||||
"source_task_id": source.Task.TaskID,
|
||||
@@ -884,6 +887,9 @@ func (t *ToolSet) relatedTaskOverviews() []map[string]any {
|
||||
item["recent_findings"] = recentFindings
|
||||
recentFacts := make([]map[string]any, 0, len(facts))
|
||||
for _, fact := range facts {
|
||||
if hidden(fact.ID) {
|
||||
continue // folded into this source's cold_digests — shown there (§2/§6.2)
|
||||
}
|
||||
m := inheritedMap(compactNode(fact), source.Task.TaskID)
|
||||
m["summary"] = budget.take(m["summary"], 400)
|
||||
if from := factFrom[fact.ID]; from > 0 && terminalIntent[from] {
|
||||
@@ -899,7 +905,14 @@ func (t *ToolSet) relatedTaskOverviews() []map[string]any {
|
||||
}
|
||||
item["recent_facts"] = recentFacts
|
||||
|
||||
recentDone := recentTerminalIntents(ts, relatedOverviewMaxIntentsPerTask)
|
||||
recentDoneRaw := recentTerminalIntents(ts, relatedOverviewMaxIntentsPerTask)
|
||||
recentDone := recentDoneRaw[:0] // in-place filter: drop this source's folded intents (§2)
|
||||
for _, intent := range recentDoneRaw {
|
||||
if hidden(intent.ID) {
|
||||
continue
|
||||
}
|
||||
recentDone = append(recentDone, intent)
|
||||
}
|
||||
for _, intent := range recentDone {
|
||||
intent.Inherited = true
|
||||
intent.SourceTaskID = source.Task.TaskID
|
||||
@@ -928,6 +941,15 @@ func (t *ToolSet) relatedTaskOverviews() []map[string]any {
|
||||
}
|
||||
}
|
||||
item["recent_intent_results"] = intentResults
|
||||
// §2 cross-task: the source task's folded cold region, read-only. Members are
|
||||
// resolvable via expand_digest(id)/node_detail(id), which search source tasks.
|
||||
if cds := activeDigestBodies(ts); len(cds) > 0 {
|
||||
for _, cd := range cds {
|
||||
cd["inherited"] = true
|
||||
cd["source_task_id"] = source.Task.TaskID
|
||||
}
|
||||
item["cold_digests"] = cds
|
||||
}
|
||||
if statsErr == nil {
|
||||
item["node_stats"] = stats
|
||||
}
|
||||
|
||||
+75
-10
@@ -119,9 +119,10 @@ func (t *ToolSet) coldDigestOverview() (digests []map[string]any, index []map[st
|
||||
}
|
||||
|
||||
// digestMemberEntry builds the compact per-member view expand_digest returns —
|
||||
// same shape as recent_facts / recent_done_intents (§6.1 middle level).
|
||||
func (t *ToolSet) digestMemberEntry(id int64) map[string]any {
|
||||
n, _ := t.ts.GetNode(id)
|
||||
// same shape as recent_facts / recent_done_intents (§6.1 middle level). store is
|
||||
// the digest's OWNING store (the current task, or a read-only source task §2).
|
||||
func (t *ToolSet) digestMemberEntry(store *db.ExplorationStore, id int64) map[string]any {
|
||||
n, _ := store.GetNode(id)
|
||||
if n == nil {
|
||||
return map[string]any{"id": id, "missing": true}
|
||||
}
|
||||
@@ -136,6 +137,60 @@ func (t *ToolSet) digestMemberEntry(id int64) map[string]any {
|
||||
return m
|
||||
}
|
||||
|
||||
// activeDigestBodies returns [{id, body, member_count}] for a store's active
|
||||
// digests — the folded cold region as flat bodies (§6.1). Shared by the current
|
||||
// task overview and the read-only related-task overview (§2 cross-task reuse).
|
||||
func activeDigestBodies(store *db.ExplorationStore) []map[string]any {
|
||||
ads, err := store.ActiveDigests()
|
||||
if err != nil || len(ads) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]map[string]any, 0, len(ads))
|
||||
for _, d := range ads {
|
||||
var p struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
_ = json.Unmarshal(d.Payload, &p)
|
||||
ms, _ := store.DigestMembers(d.ID)
|
||||
out = append(out, map[string]any{"id": d.ID, "body": p.Body, "member_count": len(ms)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// hiddenMembersFor returns a predicate telling whether a member is hidden (folded
|
||||
// into an active digest AND still cold) in the given store — so a source task's
|
||||
// overview folds exactly the way that task folds itself (§2 cross-task: "当前任务
|
||||
// 什么展示逻辑,关联任务就什么逻辑"). A revived (now hot) covered member is NOT
|
||||
// hidden (§6 render-time revival check). Returns a never-hidden predicate when the
|
||||
// store has no digests.
|
||||
func hiddenMembersFor(store *db.ExplorationStore) func(int64) bool {
|
||||
covered, err := store.CoveredMembers()
|
||||
if err != nil || len(covered) == 0 {
|
||||
return func(int64) bool { return false }
|
||||
}
|
||||
var hot map[int64]bool
|
||||
if cg, _, err := loadColdGraph(store); err == nil {
|
||||
hot = cg.hotSet()
|
||||
}
|
||||
return func(id int64) bool { _, c := covered[id]; return c && !hot[id] }
|
||||
}
|
||||
|
||||
// resolveDigest finds a digest node by id in the current task, else in a direct
|
||||
// source task (read-only, §2). Returns the node, its owning store, and the source
|
||||
// task id (0 = current task).
|
||||
func (t *ToolSet) resolveDigest(id int64) (*db.Node, *db.ExplorationStore, int64) {
|
||||
if n, _ := t.ts.GetNode(id); n != nil && n.Kind == db.KindDigest {
|
||||
return n, t.ts, 0
|
||||
}
|
||||
srcs, _ := t.ts.DirectSourceStores()
|
||||
for _, s := range srcs {
|
||||
if n, _ := s.Store.GetNode(id); n != nil && n.Kind == db.KindDigest {
|
||||
return n, s.Store, s.Task.TaskID
|
||||
}
|
||||
}
|
||||
return nil, nil, 0
|
||||
}
|
||||
|
||||
// expandDigest returns a digest's covered members as a compact list (§6.1). It is
|
||||
// a distinct tool from node_detail because it returns a LIST of members, not one
|
||||
// node's full detail.
|
||||
@@ -154,25 +209,35 @@ func (t *ToolSet) expandDigest() actool.CoreTool {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
n, _ := t.ts.GetNode(in.ID)
|
||||
if n == nil || n.Kind != db.KindDigest {
|
||||
return jsonResult(map[string]any{"error": fmt.Sprintf("#%d 不是 digest 节点", in.ID)})
|
||||
n, store, srcTaskID := t.resolveDigest(in.ID)
|
||||
if n == nil {
|
||||
return jsonResult(map[string]any{"error": fmt.Sprintf("#%d 不是 digest 节点(本任务或直接关联任务里都没找到)", in.ID)})
|
||||
}
|
||||
var p struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
_ = json.Unmarshal(n.Payload, &p)
|
||||
members, _ := t.ts.DigestMembers(in.ID)
|
||||
members, _ := store.DigestMembers(in.ID)
|
||||
list := make([]map[string]any, 0, len(members))
|
||||
for _, m := range members {
|
||||
list = append(list, t.digestMemberEntry(m))
|
||||
entry := t.digestMemberEntry(store, m)
|
||||
if srcTaskID > 0 { // 关联任务的成员:只读,带继承标记(§2)
|
||||
entry["inherited"] = true
|
||||
entry["source_task_id"] = srcTaskID
|
||||
}
|
||||
list = append(list, entry)
|
||||
}
|
||||
return jsonResult(map[string]any{
|
||||
out := map[string]any{
|
||||
"id": in.ID,
|
||||
"state": n.State, // active / superseded
|
||||
"body": p.Body,
|
||||
"members": list,
|
||||
})
|
||||
}
|
||||
if srcTaskID > 0 {
|
||||
out["inherited"] = true
|
||||
out["source_task_id"] = srcTaskID
|
||||
}
|
||||
return jsonResult(out)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user