fix(digest): 多智能体评审修正 — 资产索引收敛 + 60cap 保 hot + 文档同步
对照 cold-digest-spec 的对抗式评审(20 项发现)逐条核对实现,两处真实代码问题已修: - 资产索引膨胀(评审 §6.2 高危):早前把一个 digest 落进它成员命中的【每个】asset 桶, 跨资产 digest 会复制进几十个桶、顶层条目不降反升(真实图 asset_ids 约 144 个)。改为 每个 digest 只归一个【代表资产】桶(成员锚定众数、并列取最小 id;无锚定 → 0 号兜底桶), 条目数 ≤ digest 数。 - 60 兜底截断误砍活跃上下文(评审 §6.3 高危):hot(live 前沿祖先链 / live 意图下新事实) 是活跃推理上下文,绝不能截。改为 hot 全保、60-cap 只约束【冷但未折】残余;冷区两侧 均衡保留。graph_overview 每轮计算 render 期 hot 集(兼用于复活检查)。 其余 16 项评审发现经核对,本实现已由设计选择规避/处理:全量扫描维护(静默变冷/传递翻转 盖章)、签名含锚节点、minor+major 统一走同一 group()(§3.1 在两处都生效、major 不会拆散 同父组)、提交时活跃度复核(不折入复活成员)、content_version 列+状态变更 bump、渲染期索引 (无 KindIndex 陈旧)、cold_digests 与索引并列(body 仍在顶层)、singleton 只进 recent。 文档 §6.2 同步上述解决口径(body 归属、一 digest 一桶、singleton 归 recent)。
This commit is contained in:
1 parent
4f088e519b
commit
eefd5ee2f2
2 files changed
+57
-34
No files matched your search
+37
-20
@@ -425,11 +425,13 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
|||||||
// only when it is covered AND still cold. covered_members (built from hidden) lets a
|
// only when it is covered AND still cold. covered_members (built from hidden) lets a
|
||||||
// parents/yields id pointing into a live fold stay resolvable (§6.5 dangling lineage).
|
// parents/yields id pointing into a live fold stay resolvable (§6.5 dangling lineage).
|
||||||
covered, _ := t.ts.CoveredMembers()
|
covered, _ := t.ts.CoveredMembers()
|
||||||
|
// Hot set at render time serves two §6 needs: (1) a covered member that revived
|
||||||
|
// (now hot) must reappear this round; (2) the 60-cap must never truncate hot
|
||||||
|
// (active-context) nodes — only the cold-but-unfolded region is cappable (§6.3).
|
||||||
|
// Computed every round (cheap for real graph sizes); nil map degrades safely.
|
||||||
var hotAtRender map[int64]bool
|
var hotAtRender map[int64]bool
|
||||||
if len(covered) > 0 {
|
if cg, _, err := loadColdGraph(t.ts); err == nil {
|
||||||
if cg, _, err := loadColdGraph(t.ts); err == nil {
|
hotAtRender = cg.hotSet()
|
||||||
hotAtRender = cg.hotSet()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
hidden := func(id int64) bool { _, c := covered[id]; return c && !hotAtRender[id] }
|
hidden := func(id int64) bool { _, c := covered[id]; return c && !hotAtRender[id] }
|
||||||
fr, _ := t.ts.Frontier(100)
|
fr, _ := t.ts.Frontier(100)
|
||||||
@@ -448,7 +450,8 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
out["running_intents"] = compactIntents(running, parentsOf, yieldsOf)
|
out["running_intents"] = compactIntents(running, parentsOf, yieldsOf)
|
||||||
out["recent_done_intents"] = compactIntents(recentDone, parentsOf, yieldsOf)
|
// recent_done_intents is finalized further below (after facts are known) so the
|
||||||
|
// 60-cap can balance the two lists and protect hot nodes (§6.3).
|
||||||
// done_intents_total:已结束意图(done/blocked/exhausted)总数,与 recent_done_intents
|
// done_intents_total:已结束意图(done/blocked/exhausted)总数,与 recent_done_intents
|
||||||
// 平行命名——后者只是它的最新窗口(≤15)截断视图。两键并排即自描述:"看到的是 N/总数",
|
// 平行命名——后者只是它的最新窗口(≤15)截断视图。两键并排即自描述:"看到的是 N/总数",
|
||||||
// 让 planner 去重时别把"没显示"当成"没派过",无需在提示词里另行解释。
|
// 让 planner 去重时别把"没显示"当成"没派过",无需在提示词里另行解释。
|
||||||
@@ -521,7 +524,9 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
|||||||
findingList = append(findingList, m)
|
findingList = append(findingList, m)
|
||||||
}
|
}
|
||||||
out["finding_list"] = findingList
|
out["finding_list"] = findingList
|
||||||
recentFacts := make([]map[string]any, 0, len(factNodes))
|
// Build facts, split hot (active context — a fact under a live intent) from cold
|
||||||
|
// (not-yet-folded). Hidden (folded & still cold) ones are surfaced via cold_digests.
|
||||||
|
var recentFactsHot, recentFactsCold []map[string]any
|
||||||
for _, n := range factNodes {
|
for _, n := range factNodes {
|
||||||
if hidden(n.ID) {
|
if hidden(n.ID) {
|
||||||
continue // in a cold_digest and still cold — surfaced via cold_digests (§6.2)
|
continue // in a cold_digest and still cold — surfaced via cold_digests (§6.2)
|
||||||
@@ -538,22 +543,34 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
|||||||
m["confidence"] = c
|
m["confidence"] = c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recentFacts = append(recentFacts, m)
|
if hotAtRender[n.ID] {
|
||||||
}
|
recentFactsHot = append(recentFactsHot, m)
|
||||||
// §6.3 兜底截断:未折的 settled/cold 区(recent_done_intents + recent_facts)总量 >
|
} else {
|
||||||
// cap 时按最新优先截——live(open/running) 在独立字段、永不受影响。两边【均衡】保留:
|
recentFactsCold = append(recentFactsCold, m)
|
||||||
// 较少的一侧全留,较多的一侧填剩余额度,各自至少保底 cap/2,绝不把某一侧饿到 0
|
|
||||||
// (深探索链下 hot 的 settled 意图很多,天真的"意图优先"会把 facts 挤没)。正常由压缩
|
|
||||||
// 阈值约束、不触发;触发即标注,说明压缩滞后。
|
|
||||||
const unfoldedCap = 60
|
|
||||||
if doneList, ok := out["recent_done_intents"].([]map[string]any); ok {
|
|
||||||
if keepDone, keepFacts, truncated := balancedCap(len(doneList), len(recentFacts), unfoldedCap); truncated {
|
|
||||||
out["recent_done_intents"] = doneList[:keepDone]
|
|
||||||
recentFacts = recentFacts[:keepFacts]
|
|
||||||
out["unfolded_truncated"] = true // 正常不触发;触发说明压缩滞后
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out["recent_facts"] = recentFacts // 最近事实的 {id, summary, from_intent, confidence?},详情/证据用 node_detail(id)
|
// Split the settled intents the same way: hot = ancestor of a live intent (active
|
||||||
|
// context); cold = not-yet-folded settled.
|
||||||
|
var doneHot, doneCold []*db.Node
|
||||||
|
for _, n := range recentDone {
|
||||||
|
if hotAtRender[n.ID] {
|
||||||
|
doneHot = append(doneHot, n)
|
||||||
|
} else {
|
||||||
|
doneCold = append(doneCold, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// §6.3 兜底截断:hot(活跃探索上下文——live 前沿的祖先链、live 意图下的新事实)【永不】
|
||||||
|
// 被截;60-cap 只约束【冷但未折】的残余(压缩滞后时才增长)。live(open/running) 另有字段、
|
||||||
|
// 同样不受影响。冷区两侧均衡保留:较少一侧全留、较多一侧填余额,各自保底 cap/2,绝不饿到 0。
|
||||||
|
const unfoldedCap = 60
|
||||||
|
keepColdDone, keepColdFacts, truncated := balancedCap(len(doneCold), len(recentFactsCold), unfoldedCap)
|
||||||
|
if truncated {
|
||||||
|
out["unfolded_truncated"] = true // 正常不触发;触发说明压缩滞后
|
||||||
|
}
|
||||||
|
out["recent_done_intents"] = append(
|
||||||
|
compactIntents(doneHot, parentsOf, yieldsOf),
|
||||||
|
compactIntents(doneCold[:keepColdDone], parentsOf, yieldsOf)...)
|
||||||
|
out["recent_facts"] = append(recentFactsHot, recentFactsCold[:keepColdFacts]...) // {id, summary, from_intent, confidence?};详情用 node_detail(id)
|
||||||
// cold-digest §6.1/§6.2: the folded cold region + the per-asset directory that
|
// cold-digest §6.1/§6.2: the folded cold region + the per-asset directory that
|
||||||
// collapses independent directions, plus the dangling-lineage resolver map.
|
// collapses independent directions, plus the dangling-lineage resolver map.
|
||||||
if cds, cidx := t.coldDigestOverview(); len(cds) > 0 {
|
if cds, cidx := t.coldDigestOverview(); len(cds) > 0 {
|
||||||
|
|||||||
+20
-14
@@ -48,28 +48,34 @@ func (t *ToolSet) coldDigestOverview() (digests []map[string]any, index []map[st
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// index (§6.2): asset → digests touching it. A digest with no anchored asset
|
// index (§6.2): asset → digests. Each digest lands in EXACTLY ONE bucket — its
|
||||||
// falls into the 0 bucket ("(未锚定资产)"). One digest may list under >1 asset.
|
// representative asset = the asset anchored on the most of its members (mode;
|
||||||
|
// tie-break lowest id). This is what makes the index converge: bucketing a digest
|
||||||
|
// into every asset its members touch would duplicate it across dozens of buckets
|
||||||
|
// and blow up the top-level count instead of shrinking it (asset_ids are fine-
|
||||||
|
// grained — a real task has ~144 of them). A digest whose members anchor no asset
|
||||||
|
// falls into the 0 bucket ("(未锚定资产)").
|
||||||
byAsset := map[int64]map[int64]bool{} // asset id → set of digest ids
|
byAsset := map[int64]map[int64]bool{} // asset id → set of digest ids
|
||||||
assetSet := map[int64]bool{}
|
assetSet := map[int64]bool{}
|
||||||
for dID, ms := range memByDigest {
|
for dID, ms := range memByDigest {
|
||||||
touched := map[int64]bool{}
|
counts := map[int64]int{}
|
||||||
for _, m := range ms {
|
for _, m := range ms {
|
||||||
for _, a := range assetsByNode[m] {
|
for _, a := range assetsByNode[m] {
|
||||||
touched[a] = true
|
counts[a]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(touched) == 0 {
|
rep, best := int64(0), 0
|
||||||
touched[0] = true
|
for a, c := range counts {
|
||||||
|
if c > best || (c == best && (rep == 0 || a < rep)) {
|
||||||
|
rep, best = a, c
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for a := range touched {
|
if byAsset[rep] == nil {
|
||||||
if byAsset[a] == nil {
|
byAsset[rep] = map[int64]bool{}
|
||||||
byAsset[a] = map[int64]bool{}
|
}
|
||||||
}
|
byAsset[rep][dID] = true
|
||||||
byAsset[a][dID] = true
|
if rep != 0 {
|
||||||
if a != 0 {
|
assetSet[rep] = true
|
||||||
assetSet[a] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
labels := map[int64]string{}
|
labels := map[int64]string{}
|
||||||
|
|||||||
Reference in new issue
Block a user