From a13504be38b61344914bffa732eca7f3d8b3abf5 Mon Sep 17 00:00:00 2001 From: HomeAgent Agent Date: Sun, 13 Sep 2026 20:19:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(resident):=20=E9=94=80=E6=AF=81=E9=A9=BB?= =?UTF-8?q?=E7=95=99=E5=AD=90=E6=97=B6=E6=B3=A8=E9=94=80=E5=85=B6=E5=85=A5?= =?UTF-8?q?=E7=AB=99=20inputch=EF=BC=88child/=EF=BC=89=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E4=BF=AE=E7=99=BB=E8=AE=B0=E8=A1=A8=E8=84=8F?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=B4=AF=E7=A7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:residentInboundChannel 在 create 时把 child/ 登记进共享登记表 (Plugin=resident, Owner=父),但 teardownResident 只把划入的 inputch (如 timer)归还为未分配,从未注销这条入站登记。于是每次 create/destroy 都在登记表里留下一条脏记录,且随次数单调累积。 实测(HomeAgent 侧,HΔ-Kernel v1.3.10): 后 仍列出 child/,归属 main;HomeAgent 没有任何 工具能单独注销 inputch,只能重启 homed 清掉。 修法:teardownResident 里用纯函数 inboundChannelName 算出名字并 Unregister。 不能复用 residentInboundChannel——它有重新登记的副作用。 该路径同时覆盖 destroy / reclaim / StopResidents(父退出)。 测试:TestResident_LifecycleAndNoOrphans 增加两条断言——销毁后与父退出后 child/ 都必须从登记表消失。 --- internal/agent/core/resident.go | 15 ++++++++++++++- internal/agent/core/resident_test.go | 8 ++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) 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) + } } }