Skip to content

fix(bin): 停止将 mate home 的父通道读取为任务日志 - #2

Merged
onyx-space merged 2 commits into
mainfrom
fm/firstmate-selfwake-parent-replies
Sep 10, 2026
Merged

onyx-space merged 2 commits into
mainfrom
fm/firstmate-selfwake-parent-replies

Conversation

@onyx-space

@onyx-space onyx-space commented Sep 10, 2026 •

Copy link
Copy Markdown
Owner

Intent

队长原话:「2 派」——派 worker 修 w 上分 firstmate 报的那个 firstmate 共享代码缺陷(任务已建:firstmate-selfwake-parent-replies)。

背景:remote secondmate home 里的 state/parent-replies.status 是父频道出站日志(该 home 向主 home 上报用的),但它落在该 home watcher 的 state/*.status 通配里,被当成了名叫 parent-replies 的任务状态文件。后果:

  • 它每向父上报一行,本 home 就收到一次伪任务的 signal parent-replies.status 唤醒;
  • drain 的 OPEN DECISIONS 会把出站上报折成一条“待回答”的未决项,并建议用 fm-send --resolve-key 去关闭它(错误建议);
  • heartbeat 的 actionable backstop 扫同一通配。

elmo home 的实测证据:追加 3 行 → 1 次 signal parent-replies.status 唤醒 + 3 条伪未决项。它同时指出设计文档 docs/secondmate-parent-channel.md 已写明该文件“只应由父 home 的 watcher 分类”,所以这是实现没跟上文档,不是配置问题。

What Changed

  • 在 bin/fm-parent-channel-lib.sh 中新增 fm_parent_channel_is_own_log,一个独立谓词,用于识别远程 mate home 自身的出站通道日志(state/parent-replies.status),并将其从该 home 的 task-state 扫描中排除。
  • 将这一 guard 应用于所有 state/*.status 遍历——每次 wake 信号扫描、heartbeat backstop、daemon catch-all、open-decision 与 unread folds、presentation snapshot、orphan digest,以及 wrong-home sighting 扫描——使该通道不再被误分类为名为 parent-replies 的任务(phantom signal wakes、phantom open decisions、phantom heartbeat rows)。
  • 将排除范围限定为 remote-route mate homes,保留 main home 的同名文件及所有普通任务日志按原样扫描,并在 tests/fm-parent-channel.test.sh 与 tests/fm-session-start.test.sh 中新增覆盖。

Risk Assessment

✅ Low: 该改动是一个边界清晰的单 predicate 修复,通过基于行为的测试覆盖,全面关闭了 phantom-signal/phantom-decision/phantom-heartbeat 路径,且未发现任何可达的错误结果、授权或隐私路径。

Testing

本次运行中针对真实 firstmate 生产脚本进行了验证:sourced bin/fm-watch.sh 和 bin/fm-classify-lib.sh,并对携带 state/parent-replies.status 出站 log 及真实 t1.status task log 的 remote-secondmate home 执行了 bin/fm-wake-drain.sh,对比 base commit(phantom signal/heartbeat row/OPEN DECISION 存在)与 target(phantom 消失,t1 仍被报告),并确认排除范围保持窄化(main/local home 和普通 task log 仍扫描该文件)。项目自身的集成套件——fm-parent-channel、fm-session-start、fm-daemon、fm-pending-reply、fm-captain-hold-lifecycle——全部通过。三个已报告的 failure mode 均已消除,未出现 regression;无 warnings 或 blockers。

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Per-poll watcher signal scan:remote mate home 自身的 state/parent-replies.status 不产生 phantom signal wake,而其旁的真实 task log 仍会产生 ✅ pass live Live drive:对 remote-mate home sourced 真实 bin/fm-watch.sh 的 scan_signals;base 出现 parent-replies.status + t1.status,target 仅出现 t1.status(live-before-after-transcript.txt);tests/fm-parent-channel.t…
Wake drain OPEN DECISIONS:mate 自身的出站 log 不产生 phantom open decision,也不产生错误的 fm-send --resolve-key 建议 ✅ pass live Live drive:执行真实 bin/fm-wake-drain.sh;base 在 UNREAD STATUS 下折叠出 parent-replies [key=captain-hold-x-1] needs-decision: ... 及 parent-replies note: ...,target 仅折叠出 t1 blocked: waiting on creds(live-…
Heartbeat actionable backstop:mate 自身的 parent-channel log 不作为 actionable row 出现 ✅ pass live Live drive:sourced 真实 bin/fm-watch.sh 的 heartbeat_scan_finds_actionable;base 出现 parent-replies.status + t1.status,target 仅出现 t1.status(live-before-after-transcript.txt);tests/fm-parent-channel.t…
Exclusion scoping(adversarial):main home、local-route mate home 及所有普通 task log 仍扫描 state/parent-replies.status;仅 remote-route mate home 自身的 log 被排除 ✅ pass live Live drive:predicate fm_parent_channel_is_own_log 对 main/local home 及 remote home 的普通 t1.status 返回 SCANNED,仅对 remote home 自身的 parent-replies.status 返回 EXCLUDED;对 main/local home 的 signal scan 仍出现 pare…
Digest orphan listing 及其余 fleet scan(unread surface、presentation snapshot、incremental open-decision fold)同样跳过 channel log ✅ pass live 通过真实 bin/fm-classify-lib.sh 对 scan_unread_surface_lines / status_presentation_snapshot / scan_open_decisions_incremental 进行 Live drive,仅返回 t1 行;tests/fm-session-start.test.sh 的 test_mate_home_parent_c…
Evidence: Live base-vs-target transcript(signal、heartbeat、drain、scoping、predicate)

Source: Live base-vs-target transcript(signal、heartbeat、drain、scoping、predicate)

===== parent-channel phantom-log fix — live before/after (re-driven 2026-09-10T13:13:52Z) =====

----- SIGNAL SCAN (per-poll), remote mate home -----
base:
    parent-replies.status
    t1.status
target:
    t1.status

----- HEARTBEAT backstop, remote mate home -----
base:
    parent-replies.status	150	strong:16777229:84460144:1789046032.875781546
    t1.status	56	strong:16777229:84460145:1789046032.875883587
    
target:
    t1.status	56	strong:16777229:84460139:1789046032.872508386
    

----- DRAIN (OPEN DECISIONS + UNREAD STATUS), remote mate home -----
base:
    UNREAD STATUS (new since last drain, not re-printed after this presentation):
    parent-replies note: judgement the mate appended itself
    t1 note: read me
    OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
    parent-replies [key=captain-hold-x-1] needs-decision: captain hold x: pick one
    t1 blocked: waiting on creds
    OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'
target:
    UNREAD STATUS (new since last drain, not re-printed after this presentation):
    t1 note: read me
    OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
    t1 blocked: waiting on creds
    OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'

----- SCOPING (target bin): which homes exclude state/parent-replies.status -----
  main home:
    parent-replies.status
    t1.status
  local home:
    parent-replies.status
    t1.status
  remote home:
    t1.status

----- PREDICATE fm_parent_channel_is_own_log scope (target bin) -----
  main home parent-replies.status -> SCANNED
  local home parent-replies.status -> SCANNED
  remote home parent-replies.status -> EXCLUDED
  remote home t1.status (ordinary task) -> SCANNED
Evidence: Drain OPEN DECISIONS before/after

Source: Drain OPEN DECISIONS before/after

===== DRAIN — base (phantom OPEN DECISION present) =====
UNREAD STATUS (new since last drain, not re-printed after this presentation):
parent-replies note: judgement the mate appended itself
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
parent-replies [key=captain-hold-x-1] needs-decision: captain hold x: pick one
t1 blocked: waiting on creds
OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'

===== DRAIN — target (phantom gone) =====
OPEN DECISIONS (still open, folded from the durable status logs - not just the latest line):
t1 blocked: waiting on creds
OPEN DECISIONS: close one by answering it: bin/fm-send.sh <task> --resolve-key <key> '<answer>'
Evidence: Signal + heartbeat scan before/after

Source: Signal + heartbeat scan before/after

===== SIGNAL SCAN — base (phantom present) =====
parent-replies.status
t1.status

===== SIGNAL SCAN — target (phantom gone) =====
t1.status

===== HEARTBEAT — base (phantom present) =====
parent-replies.status
t1.status


===== HEARTBEAT — target (phantom gone) =====
t1.status
Evidence: Fix summary transcript

Source: Fix summary transcript

==================================================================
firstmate parent-channel phantom-log fix — live evidence transcript
branch fm/firstmate-selfwake-parent-replies  (target 570651f vs base 300137d)
==================================================================

[1] SIGNAL SCAN (per-poll) — base vs target
    base   surfaces parent-replies.status (phantom signal) AND t1.status
    target surfaces t1.status ONLY

[2] HEARTBEAT backstop — base vs target
    base   surfaces parent-replies.status row AND t1.status
    target surfaces t1.status ONLY

[3] DRAIN (OPEN DECISIONS) — base vs target
    base:  OPEN DECISIONS listed 'parent-replies [key=captain-hold-x-1] needs-decision: captain hold x: pick one'
           plus 'parent-replies note: ...' under UNREAD STATUS
    target: OPEN DECISIONS lists 't1 blocked: waiting on creds' only

[4] DIGEST / orphan listing — target (fm-session-start.sh)
    parent-replies occurrences in real digest: 0
    task-orphan occurrences (real orphan still reported): 2

[5] SCOPING — which homes exclude state/parent-replies.status
    main  home: surfaces parent-replies.status AND t1.status  (NOT excluded — main has no channel)
    local home: surfaces parent-replies.status AND t1.status  (NOT excluded — local route publishes to parent's state/<id>.status)
    remote home: surfaces t1.status ONLY  (excluded — remote route owns state/parent-replies.status)

==================================================================

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Per-poll watcher signal scan:remote mate home 自身的 state/parent-replies.status 不产生 phantom signal wake,而其旁的真实 task log 仍会产生 ✅ pass live Live drive:对 remote-mate home sourced 真实 bin/fm-watch.sh 的 scan_signals;base 出现 parent-replies.status + t1.status,target 仅出现 t1.status(live-before-after-transcript.txt);tests/fm-parent-channel.t…
Wake drain OPEN DECISIONS:mate 自身的出站 log 不产生 phantom open decision,也不产生错误的 fm-send --resolve-key 建议 ✅ pass live Live drive:执行真实 bin/fm-wake-drain.sh;base 在 UNREAD STATUS 下折叠出 parent-replies [key=captain-hold-x-1] needs-decision: ... 及 parent-replies note: ...,target 仅折叠出 t1 blocked: waiting on creds(live-…
Heartbeat actionable backstop:mate 自身的 parent-channel log 不作为 actionable row 出现 ✅ pass live Live drive:sourced 真实 bin/fm-watch.sh 的 heartbeat_scan_finds_actionable;base 出现 parent-replies.status + t1.status,target 仅出现 t1.status(live-before-after-transcript.txt);tests/fm-parent-channel.t…
Exclusion scoping(adversarial):main home、local-route mate home 及所有普通 task log 仍扫描 state/parent-replies.status;仅 remote-route mate home 自身的 log 被排除 ✅ pass live Live drive:predicate fm_parent_channel_is_own_log 对 main/local home 及 remote home 的普通 t1.status 返回 SCANNED,仅对 remote home 自身的 parent-replies.status 返回 EXCLUDED;对 main/local home 的 signal scan 仍出现 pare…
Digest orphan listing 及其余 fleet scan(unread surface、presentation snapshot、incremental open-decision fold)同样跳过 channel log ✅ pass live 通过真实 bin/fm-classify-lib.sh 对 scan_unread_surface_lines / status_presentation_snapshot / scan_open_decisions_incremental 进行 Live drive,仅返回 t1 行;tests/fm-session-start.test.sh 的 test_mate_home_parent_c…
  • bash tests/fm-parent-channel.test.sh
  • bash tests/fm-session-start.test.sh
  • bash tests/fm-daemon.test.sh
  • bash tests/fm-pending-reply.test.sh
  • bash tests/fm-captain-hold-lifecycle.test.sh
  • Live base-vs-target drive:sourced 真实 bin/fm-watch.sh 的 scan_signals + heartbeat_scan_finds_actionable,真实 bin/fm-classify-lib.sh 的 scan_open_decisions,并对真实 remote-mate home 执行了真实 bin/fm-wake-drain.sh(base 300137d vs target 570651f)
  • 通过真实 bin/fm-classify-lib.sh 对 scan_open_decisions_incremental、scan_unread_surface_lines、status_presentation_snapshot 进行 Live drive
  • 对 fm_parent_channel_is_own_log 在 main/local/remote home 及普通 task log 上进行 Live predicate scope check
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

A remote secondmate home publishes its captain-facing outcomes on its own
state/parent-replies.status, which the parent's remote reply adapter mirrors into
the parent's state/<mate-id>.status. That file sits inside the mate home's own
state/*.status walks, so every published line was classified as a task named
`parent-replies`: a phantom `signal parent-replies.status` wake on each append, a
phantom open decision the drain offered to close with fm-send --resolve-key, and
a phantom row in the heartbeat backstop. The design already said the parent
classifies that stream (docs/secondmate-parent-channel.md); the scans had not
implemented it.

Add one predicate, bin/fm-parent-channel-lib.sh's fm_parent_channel_is_own_log,
which resolves the home's channel once and answers whether a path is that
outbound log, and consult it in every task-state walk: the watcher's per-poll
signal scan and heartbeat backstop, the daemon's catch-all scan, the fleet-wide
open-decision and unread folds, the presentation snapshot, the digest's orphan
listing, and the task-record divergence report. bin/fm-pending-reply-lib.sh's
wrong-home scan drops its own copy of the same rule in favor of the predicate.

The exclusion is scoped to the home that owns the log: only a remote-route mate
home resolves a channel at that path, so a main home's same-named file, a
local-route mate home, and every ordinary state/<task>.status log are scanned
exactly as before.

Cover the predicate's scope and each scan family in tests/fm-parent-channel.test.sh,
and the digest's orphan listing in tests/fm-session-start.test.sh.
…fm-parent-channel-lib.sh` source(后者又 source bin/fm-secondmate-parent-lib.sh),使新的 fm_parent_channel_is_own_log 谓词在每个 state/*.status walk 中可用。三个测试 fixture 复制了一个包含 fm-classify-lib.sh(或传递性 source 它的 fm-pending-reply-lib.sh / fm-branch-outcome.sh)但不包含这两个新依赖文件的最小化 bin/ 目录树,因此 source 复制后的脚本失败,报错 'fm-parent-channel-lib.sh: No such file or directory'。这正好产生了三处报告的失败:fm-remote-transport-lanes (secondmate, shard 1)、fm-afk-return (afk, shard 2)、fm-pi-branch-extension (standalone, shard 2)。修复:将 fm-parent-channel-lib.sh 和 fm-secondmate-parent-lib.sh 添加到以上三个 fixture 各自的复制列表中。已在本地验证:fm-afk-return 和 fm-remote-transport-lanes 现已通过,本 PR 自身的 fm-parent-channel 测试通过,所有已编辑文件的 bash -n 检查均无错误。fm-pi-branch-extension 的 broken-fixture 路径通过复现 fixture 并确认 fm-branch-outcome.sh 现在能正常 source 来验证(其本地全量测试运行还额外遇到一个预先存在的、与本改动无关的 ERR_MODULE_NOT_FOUND,涉及全局安装的 @earendil-works/pi-ai package,并非本次改动所致)。
@onyx-space
onyx-space merged commit c25e04a into main Sep 10, 2026
26 of 27 checks passed
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.

1 participant