Skip to content

[PR] fix: persona-lineage follow must not redirect host calls into a fork while the parent is still active - #3648

Closed
JiewiW wants to merge 2 commits into
ranxianglei:masterfrom
JiewiW:2026-10-18_fix-persona-follow-active-parent
Closed

JiewiW wants to merge 2 commits into
ranxianglei:masterfrom
JiewiW:2026-10-18_fix-persona-follow-active-parent

Conversation

@JiewiW

@JiewiW JiewiW commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #3647

Problem

The #2601 persona-lineage follow (livePersonaDescendant) redirected a host-stamped nativeCaller management call (compress / decompress / acp_status) into a |sub: persona fork whenever the fork was fresher than the raw parent:

ts return personaDescendantsOf(parent.id).find((s) => (s.lastSeen ?? 0) > (parent.lastSeen ?? 0));

That recency rule also matches the active-parent shape: the parent kept receiving turns after the fork was minted (parallel persona forks, or subagent lanes sharing the base id), and the fork happens to be the fresher one. Re-routing the host's call into the fork's namespace then fails with "every ref is unknown to this session" / cannot-anchor — the same breaker #2601 exists to cure, reproduced in reverse. Field incident: a dsh (DeepSeek Harness) session under fusion-board subagent traffic suffered 11 consecutive compress failures, 8 of them cannot-anchor, all behind the following live fork …(#2601) redirect log, with the fork refs counter oscillating (three parallel forks sharing the base id).

#2948 (ref-less aux lanes) and #3405 (follow the fork holding the cited refs) are complementary and do not cover this: both still pick a fork when the parent is live, because neither consults whether the parent saw traffic after the fork was born.

Root cause

lastSeen only says which session is freshest; it cannot distinguish a frozen parent (the #2601 legit shape — the fork is the continuation, the host's refs can only live in the fork) from a still-active parent (the incident shape — the parent's refs live in its own namespace; redirecting them into a fork is wrong).

Fix

Gate the follow on birth order, not freshness. Session.createdAt is stamped at session mint time (src/session.ts createSession); the fork is minted when the persona split happens. Follow the fork only when the parent saw no traffic after the fork was born:

ts return personaDescendantsOf(parent.id).find((s) => (s.createdAt ?? 0) > (parent.lastSeen ?? 0));

  • Frozen parent (#2601): fork.createdAt > parent.lastSeen → still follows the fork. No regression.
  • Active parent (incident): parent.lastSeen lands after fork.createdAt → parent stays authoritative, no redirect.
  • Switch-back self-heals: when the user returns to the raw persona, the parent's lastSeen advances past every fork's createdAt → no follow.

Complementary with #3405: that PR decides which fork to follow when a follow happens (by refs ownership); this change decides whether a follow happens at all (the parent must be frozen). Both can land independently.

Tests

New tests/persona-fork-active-parent-guard.test.ts:

  1. Frozen-parent shape still follows the fork (regression guard for #2601).
  2. Active-parent shape (parent traffic after the fork was born, fork fresher) stays on the parent — asserts the reply does not contain |sub:.

Existing issue2601-persona-fork-lineage (6 unit + 1 e2e) and issue2948-aux-lane-lineage-guard (2 e2e) suites pass unchanged. Typecheck clean.

…while the parent is still active

livePersonaDescendant picked a follow target by freshness alone
(fork.lastSeen > parent.lastSeen). A parallel persona fork / subagent lane
sharing the base id with a still-active parent looks exactly like a frozen
parent case, so host-stamped management calls (compress/decompress/
acp_status) got redirected into the fork's ref namespace and failed
"every ref is unknown to this session" / cannot-anchor - 8 consecutive
failures in a dsh fusion-board session.

Gate the follow on birth order: follow a fork only when the parent saw NO
traffic after the fork was minted (fork.createdAt > parent.lastSeen). A
parent still receiving turns stays authoritative; its refs live in its own
namespace. ranxianglei#2601 legit shape (frozen parent -> fork continuation) and the
switch-back self-heal are preserved.

Regression: tests/persona-fork-active-parent-guard.test.ts - frozen-parent
still follows the fork; active-parent does not redirect. issue2601 /
issue2948 suites stay green. See ranxianglei#3647.
@ranxianglei ranxianglei changed the title fix: persona-lineage follow must not redirect host calls into a fork while the parent is still active [PR] fix: persona-lineage follow must not redirect host calls into a fork while the parent is still active Oct 18, 2026
@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

查重: 不重复。 与在途 PR #3405(「persona conversation keys survive proxy restarts; management calls follow the fork holding cited refs」#3397)同属「persona lineage follow 误路由」家族但机制不同:#3405 回答「跟哪个 fork」(候选内 cited-ref frontier 优先),本 PR 回答「该不该跟」(birth-order 门禁:frozen parent 才跟)。上游关联 #2601(follow 规则本源)、#2948(holdsRefs 守卫)均已合入且与本改动正交。open+closed 检索无其他条目覆盖 active-parent 形状。

方向关(独立复核,不照抄 issue 结论):

中间状态:CI(head 464ce986e)静态门禁全绿(static-gates / docs-sync / kernel-guard / version-guard / lane-guard / config-surface-guard 等),test 分片 2/6 绿、4 个在跑。本地定向验证(新增测试 + #2601/#2948 回归套件 + typecheck)进行中,结构化八问审核结论随后单独发布。

@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

PR 审核结论(独立审核,非复读 issue):

① 方向关:读原 issue #3647 + 独立推演后确认方向正确。根因在选择准则本身——fork.lastSeen > parent.lastSeen 只能回答「谁最新」,无法区分两种形状:(a) frozen parent(fork 是延续,refs 只能在 fork,#2601 正形);(b) active parent(fork 只是并行车道,refs 在 parent 自己的命名空间)。现有守卫都不覆盖形状 (b):#2948 holdsRefs 只排除无 ref 车道(并行 fork 持有 refs),#3405(在途)的候选集与 recency fallback 仍是新鲜度准则。birth-order 门禁是打到根因的最小修复。

② 回归关(逐项清点既有行为,全部本地实测):

③ 代码关:单行谓词替换 + 注释块重写。注释准确解释了 why(出生顺序 vs 新鲜度、事故引用),符合本文件惯例。(s.createdAt ?? 0) 的 ?? 0 对必填字段是冗余防御,但与相邻 (s.lastSeen ?? 0) 风格一致,可接受。测试代码质量达标:hermetic XDG rig(导入前设 env)、listen(0) 端口(确定性,§7.2)、刻意用 peekSession 而非 getSession(避免 lastSeen 刷新破坏前置条件,有注释说明)。

④ 测试有效性关:断言有牙且已做红绿双向验证——

⑤ diff 整洁度:单 commit 落在当前 master tip 上(无陈旧 base),3 文件。一个 nit:.gitignore 的 .esbuild-tmp/ hunk 与本修复无关(测试文件无任何引用,属本地 esbuild 临时目录重定向的环境适配)——零功能影响,建议保留但请作者确认;若你的本地 runner 不用 ESBUILD_TMPDIR 重定向,合并前删掉它更干净。非阻塞。

八问:

  1. 是什么问题:dsh 用裸会话 id 发 compress/decompress/acp_status 时,lineage follow 按「哪个 |sub: fork 最近活跃」选目标,parent 仍活跃时若某并行 fork 恰好更新,host 调用被误路由进 fork 命名空间,refs 全 unknown → cannot-anchor 连片失败。
  2. 什么原因导致的:选择准则用新鲜度,无法区分「frozen parent(fork 是延续)」与「active parent(fork 是并行车道)」;【Bug】livePersonaDescendant redirects host-stamped management calls to auto-review's aux lane (ref registry always empty) → manual compress of the entire session fails | 【Bug】livePersonaDescendant 把宿主盖章的管理调用重定向到 auto-review 的 aux lane(ref 注册表恒空)→ 手动 compress 整会话失效 #2948/[PR] fix: persona conversation keys survive proxy restarts; management calls follow the fork holding cited refs (#3397) #3405 两个已有守卫都不咨询 parent 是否在 fork 诞生后仍活跃。
  3. 影响范围与严重程度:仅 plugin-mode 宿主原生 agent 车道(dsh 等),触发条件 = 裸 id 会话 + 存活 |sub: 后代 + parent 分叉后仍收流量;触发后会话内压缩能力持续不可用(连续 cannot-anchor),但不损坏数据、不影响模型流量(模型请求走各自指纹路由,不受此路径影响)。
  4. 怎么修复的:准则从新鲜度换成出生顺序——仅当 fork.createdAt > parent.lastSeen(fork 诞生后 parent 零流量)才 follow。
  5. 是否有回归问题:无——上述②逐项清点,两套既有套件实测全绿,红绿验证证明新测试钉住本 bug。
  6. 是否破坏缓存:不涉及(prefix cache / 摘要载体 / fold 状态均未触碰)。
  7. 是否改了设计方向:不涉及;lineage-follow 机制延续,仅换选择准则。与 [PR] fix: persona conversation keys survive proxy restarts; management calls follow the fork holding cited refs (#3397) #3405 正交可组合:先门控候选集(本 PR),再在候选内做 cited-ref 命中优先([PR] fix: persona conversation keys survive proxy restarts; management calls follow the fork holding cited refs (#3397) #3405)——已在 [PR] fix: persona conversation keys survive proxy restarts; management calls follow the fork holding cited refs (#3397) #3405 线程留交叉备注,其 rebase 时候选过滤器应继承本门禁。
  8. 是否引入了新的配置:不涉及(无 env/config/默认值变更)。

证据汇总:本地定向运行——新测试绿;红检(还原旧谓词→case 2 红→恢复→绿);issue2601 7/7;issue2948 2/2;typecheck 干净。远程 CI:ubuntu-22×3 shards + static-gates 已绿,ubuntu-24/windows 运行中;mergeable: true(无冲突,blocked 仅为检查未齐)。

判定:质量达标,可合(等 CI 全绿后由人工合并——agent 无合并权限,PR 链接:https://github.com/ranxianglei/billion-context/pull/3648)。

@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

审核结论 — 结构化八问

  1. 是什么问题:dsh 等宿主的管理调用(compress/decompress/acp_status)始终盖原始会话 id;会话因 persona 指纹分裂出 |sub: fork 后,bili 的 lineage-follow 决定「是否把调用重定向进 fork」时只看「谁更新」。并行 fork(共享 base id 的子代理车道)恰好比仍活跃的父会话更新时,调用被误路由进错误 fork 的命名空间 → 该会话压缩连续失败(现场事故:11 连败、8 次 cannot-anchor)。
  2. 什么原因导致的:选择准则缺维度——lastSeen 只能回答「谁最新」,无法区分「冻结父(fork 是延续,[Bug] dsh 会话反复 model/provider 切换时主对话迁移到 |sub:<fp> persona fork,宿主盖章的 compress/decompress/acp_status 仍解析在陈旧父 id —— ref 跨空间不可解析直至熔断 #2601 正形)」与「活跃父(fork 只是并行车道,事故形)」。误路由是根因,cannot-anchor 是下游症状;报告者的根因定位成立。
  3. 影响范围与严重程度:仅 plugin 模式 nativeCaller 无 witness 管理调用路径(proxy 模式、witness 路径不受影响)。触发条件窄:同一 base id 下多个持 ref 的并行 fork + 父会话仍有流量 + 某 fork 更新。命中时该会话压缩能力完全不可用(可用性级),但全部失败都是拒绝、无状态损坏、无数据丢失。
  4. 怎么修复的:follow 门禁从新鲜度比较改为出生顺序比较——仅当 fork.createdAt > parent.lastSeen(fork 诞生后父再无流量 = 冻结)才跟 fork;活跃父保持权威。switch-back 自然自愈(父的新流量把 lastSeen 推过 fork.createdAt)。
  5. 是否有回归问题(逐项审计既有行为):
  6. 是否破坏缓存:不涉及——不触碰前缀缓存/重试/增量游标/去重键,只改会话路由判定。
  7. 是否改了设计方向:否。「单写者 + lineage follow」框架内收紧一个路由谓词(新鲜度→出生顺序);与 [PR] fix: persona conversation keys survive proxy restarts; management calls follow the fork holding cited refs (#3397) #3405 正交可组合(先门禁候选集,再在候选内做 cited-ref frontier 优先),已对照 [PR] fix: persona conversation keys survive proxy restarts; management calls follow the fork holding cited refs (#3397) #3405 diff 确认组合语义无冲突。
  8. 是否引入新的配置:无——无新 env/config/默认值。

验证证据

  • 本地定向:persona-fork-active-parent-guard(新)+ issue2601-persona-fork-lineage + issue2948-aux-lane-lineage-guard + dsh-persona-fingerprint = 21/21 绿;
  • 红绿验证:把门禁行临时还原为旧规则 → 新测试失败;还原修复 → 通过。测试有牙,能抓住本 bug 回归;
  • npm run typecheck 干净;
  • CI(head 464ce986e)全绿:6/6 test 分片 + 静态门禁全过(e2e 门控车道按惯例 skip)。

行为变更披露(旧→新)

代码质量(外部提交硬标准)

逐项通过:注释讲清 discriminator 与事故出处且与代码事实一致;无死代码/魔法数字(复用既有常量);测试断言有牙(part 2 精确复现事故形状);diff 收敛到目的。一处小点:.gitignore 新增 .esbuild-tmp/(作者本地 esbuild 临时目录,注释指向 persona-fork 测试)——纯追加、无运行时影响、对他人无害,接受,不强删。

协同与排序

#3405 当前对 master 为 dirty(src/plugin.ts 热文件冲突,其改动面更大:order-persist/restart/session-resolve)。两 PR 任一顺序落地都语义成立,后落者需 rebase;建议本 PR(小、独立闭环)先行。

判定:可合(合并由人执行)。

Resolve the livePersonaDescendant conflict by composing both changes:
master's ranxianglei#3397 ref-frontier preference now picks among candidates gated
by the ranxianglei#3647/ranxianglei#3648 birth-order rule (fork.createdAt > parent.lastSeen).
An active parent empties the candidate set, so neither the recency
fallback nor the no-fit fallback can land on a parallel fork; a frozen
parent keeps ranxianglei#2601 follow with ref-fit preference. .gitignore takes the
union of both sides.
@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

冲突已解决,PR 恢复可合并。说明:

原因:你开 PR 后 master 前进了——#3405 已合并并随 v0.1.204 发布,它重写了 livePersonaDescendant(加了 maxCitedRefNum/highestMintedRefNum 与 ref-frontier 偏好),与本 PR 改的是同一函数,产生文本冲突(mergeable_state 由 clean 变 dirty)。

解法(组合,非二选一):我把 origin/master merge 进你的分支,冲突按两者意图的并集解决——

证据(合并头本地定向验证):tests/persona-fork-active-parent-guard.test.ts(两例)+ tests/issue2601-persona-fork-lineage.test.ts(7)+ tests/issue2948-aux-lane-lineage-guard.test.ts(2)+ tests/issue3397-persona-restart-stability.test.ts(4)= 14/14 绿;npm run typecheck 干净。全量回归看远程 CI(新 head 已在跑)。

新 head:b0ccffae3(单一 merge commit,你的原始提交与署名未动)。请过目组合后的函数;CI 绿后即可人工合并。另:#3647 收到了 v0.1.204 的独立现场证据(aux helper persona 形状,与你修的机制完全同族),已并入该 issue 记录。

@ranxianglei

Copy link
Copy Markdown
Owner

(ai: zhipuai-lb/glm-5.3)

已并入合并列车 #4525(master 6d7c587,合并方式 merge commit,提交保留原作者署名)。本 PR 关单收尾;若列车中对应提交出问题,回滚点为 #4525 单提交 revert。

感谢 @JiewiW 的贡献——你的提交以原作者署名随列车进入 master(合并方式为 merge commit),仓库贡献者列表可见。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: persona-lineage follow must not redirect host calls into a fork while the parent is still active

2 participants