diff --git a/internal/agent/core/resident.go b/internal/agent/core/resident.go index dcb0a55..1f83265 100644 --- a/internal/agent/core/resident.go +++ b/internal/agent/core/resident.go @@ -198,8 +198,13 @@ func (a *Agent) SpawnResident(opts ResidentOptions) (ResidentInfo, error) { func (a *Agent) residentParentSource() string { return "parent/" + string(a.id) } // residentInboundChannel 是"父接收某个子的消息"的 inputch 名(登记进登记表可见)。 +// +// inboundChannelName 只拼名字,不产生副作用(注销路径要用它算出同一个名字, +// 不能再去调 residentInboundChannel——那会顺手把刚摘掉的登记又写回去)。 +func inboundChannelName(childID string) string { return "child/" + childID } + func (a *Agent) residentInboundChannel(childID string) string { - ch := "child/" + childID + ch := inboundChannelName(childID) if reg := a.io.ChannelRegistry(); reg != nil { // 归属父自己:它是父的入站 inputch。 _ = reg.Register(agentIO.InputChannel{Name: ch, Plugin: "resident", Owner: string(a.id)}) @@ -246,6 +251,14 @@ func (a *Agent) teardownResident(rc *residentChild) { for _, ch := range rc.inputChs { _ = reg.Assign(ch, "", 0) } + // 注销"父接收该子消息"的入站 inputch(child/)。 + // + // 它由 residentInboundChannel 在 create 时登记(Owner=父),销毁时必须 + // 一并摘掉:登记表是共享的、按 name 全局唯一,残留会随 create/destroy + // 次数单调累积脏数据。实测:destroy 后 child/ 仍挂在根 agent 名下, + // 而外部没有任何工具能单独注销 inputch,只能重启 homed 清。 + // 注意用纯函数算名字,不要再走 residentInboundChannel(会重新登记)。 + reg.Unregister(inboundChannelName(rc.id)) } } diff --git a/internal/agent/core/resident_test.go b/internal/agent/core/resident_test.go index a451729..0aefc4f 100644 --- a/internal/agent/core/resident_test.go +++ b/internal/agent/core/resident_test.go @@ -142,6 +142,11 @@ func TestResident_LifecycleAndNoOrphans(t *testing.T) { if ch, _ := reg.Lookup("sub/in"); ch.Owner != "" { t.Fatalf("销毁后 inputch 应回到未分配:%+v", ch) } + // 父的入站 inputch(child/)必须在销毁时一并注销,否则登记表残留脏数据—— + // HomeAgent 实测:destroy 后 child/ 仍挂在根 agent 名下,且无工具可单独注销。 + if inbound, ok := reg.Lookup("child/child-1"); ok { + t.Fatalf("销毁后父的入站 inputch 应被注销,实际残留:%+v", inbound) + } if err := parent.DestroyResident("child-1"); err == nil { t.Fatal("重复销毁应报错") } @@ -159,6 +164,9 @@ func TestResident_LifecycleAndNoOrphans(t *testing.T) { if _, err := osStat(filepath.Join(dir, "residents", id)); err == nil { t.Fatalf("子 %s 的 temp 目录应被丢弃", id) } + if _, ok := reg.Lookup(inboundChannelName(id)); ok { + t.Fatalf("父退出后子 %s 的入站 inputch 应被注销", id) + } } }