Skip to content

fix(remote-chat): key a remote chat by its room, so one published room is one row (#2797) - #2801

Merged
Birfy merged 1 commit into
mainfrom
dev/2797-remote-room-one-row
Nov 10, 2026
Merged

Birfy merged 1 commit into
mainfrom
dev/2797-remote-room-one-row

Conversation

@Birfy

@Birfy Birfy commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: FlyBot 实现线

Fixes #2797
Owner: Chief
Reviewer: Reviewer
Tip SHA: 1b2b39c
Base: main 5b8d980

What changed

Root cause (what the code showed). core/src/host/remote-chat.js keyed a remote chat by the
handle it was published on (chatOfHandle), so one identity published under several handles opened
several chats. The publisher names a room once per member of that room — every member record
carries the same name (the room's own title, e.g. 123456 · 刘红升 2, flybot682b8a43, FlyBot) at
the same home, with a different member handle each — which is exactly the owner's screenshot: the
same title 8+ times, and every row is a real chat record with the same room name. The same shape
also explains the second-order case: when the handle that carried a room changes between polls, the
old chat is kept as gone and a new row appears beside it.

  • open() now reuses the chat that already holds that room (chatOfRoom: the same name at the
    same home) when no chat holds the handle. The stored remote.handle is not rewritten — the
    chat's model route (flybot-remote/<handle>) names it.
  • sync() opens a room once (the rest of its members only keep it from going stale) and keeps the
    chat live while any member of that room is still published, instead of marking it gone
    because the particular member handle vanished.
  • deliver() needed no change: a line from another member of the room reaches the same chat through
    open(), so the reply, the thinking row and the history stay in the one room chat.
  • The list layer only: nothing is folded in the UI, and no chat record is deleted, so a room's
    history is the history it already had. A record with no home, or with no name of its own, and a
    chat that recorded neither, keep the plain per-handle rule.
  • Control cases are pinned in the same test file: two names at one home, and one name at two homes
    (two friends), stay two rows — the room rule never folds a list.
  • Bot Social's publish protocol is untouched; the fix is on the FlyBot side (docs/subsystems/core-host.md
    updated, changelog.d/2797-remote-room-one-row.fixed.md added).

Ran at this tip

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test --test-timeout=120000 \
    test/remote-chat.test.js test/remote-room.test.js test/friend-chat-awaiting.test.js \
    test/new-chat.test.js test/mention-style.test.js
# tests 163  # pass 163  # fail 0

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test --test-timeout=120000 \
    test/remote-chat.test.js          # red first, on a pristine extract of main 5b8d980f
# tests 25  # pass 22  # fail 3   (the 4 new #2797 cases: 3 red, the control case green)

$ TZ=UTC npm --prefix core test
# tests 3194  # pass 3190  # fail 2  # skipped 2
# the 2 failures are core/test/check-arch-docs.test.js "empty diff passes" and "the gate never
# runs git through a shell": this box has no git binary, so they fail identically on an
# untouched extract of main 5b8d980f (5 pass / 2 fail there, same two names)

$ npm --prefix plugins/social test    # tests 619  pass 619  fail 0
$ npm --prefix plugins/tree test      # tests 143  pass 143  fail 0
$ npm --prefix plugins/timeline test  # tests 161  pass 161  fail 0
$ npm --prefix plugins/workflow test  # tests 528  pass 528  fail 0
$ node --test ".github/scripts/*.test.mjs" "policy/*.test.mjs"   # tests 666  pass 664  fail 0  skipped 2
$ npm run lint:interop && npm run test:interop                   # 37 pass / 37
$ npm run test:live-e2e                                          # 34 pass / 34
$ node scripts/lint-plugins.mjs                                  # ok (packed @flybot/* imported)
$ node core/scripts/build-client.mjs --check                     # ok, no rebuild needed (host-side change)
$ node scripts/ds-bot-patches.mjs check                          # ok, 10 patches apply; core/ = ds-bot/ + patches
$ node core/scripts/check-arch-docs.mjs                          # ok, 6 changed files (core-host.md updated)
$ node .github/scripts/changelog.mjs check                       # ok, 22 fragments well formed

Not run, and why — this box has no git binary and no deepseek-harness checkout:
node scripts/ds-bot-patches.mjs canonical (needs git diff of the series), the arch-docs gate's
own git log/git diff path (proved instead by feeding the six changed paths to
check-arch-docs.mjs through a local git stand-in: OK with the doc, FAIL without it, so the gate
still bites), and dev/bench.sh smoke (needs a harness checkout and git).

Scope

  • core/src/host/remote-chat.js — the room key, open(), sync().
  • core/test/remote-chat.test.js — four #2797 cases (red first, listed above).
  • patches/ds-bot/files/src/host/remote-chat.js, patches/ds-bot/files/test/remote-chat.test.js — the generated mirrors of the two above (generate needs git here, so they were written to match and check proves they do).
  • docs/subsystems/core-host.md — the remoteChats.sync bullet now states the room rule.
  • changelog.d/2797-remote-room-one-row.fixed.md — one English line, user-visible.

Not touched: Bot Social's publish protocol, .github/, policy/, hold, any other open PR.

Checkpoint

{
  "pr": 2801,
  "claim": "Chief",
  "issue": 2797,
  "summary": "A friend's room reaches this home once per member, every record carrying the same room title at the same home, and remote-chat.js keyed a chat by the member handle, so one group filled the friends' list with 8+ rows. open() now reuses the chat holding that name at that home, and sync() opens a room once and keeps it live while any member is published. The stored remote.handle is never rewritten (the chat's model route names it), and a line from another member lands in that chat. Two names at one home, and one name at two homes, stay two rows.",
  "files": ["core/src/host/remote-chat.js", "core/test/remote-chat.test.js", "patches/ds-bot/files/src/host/remote-chat.js", "patches/ds-bot/files/test/remote-chat.test.js", "docs/subsystems/core-host.md", "changelog.d/2797-remote-room-one-row.fixed.md"],
  "checks": { "ran": "npm --prefix core test; core/test/remote-chat.test.js; plugins; ds-bot-patches check", "result": "green", "at": "1b2b39c446ab3e8a5da84022b28ff17eda31b9f0" },
  "next": "none — ready for review"
}

…m is one row (#2797)

A friend's room reaches this home once per member of that room: every member record carries
the same name — the room's own title — at the same home, and remote-chat.js keyed a chat by the
member handle, so the friends' list showed one room as many rows (the owner's screenshot has the
same row eight times and reads it as one group).

open() now reuses the chat holding that name at that home, and sync() opens a room once and keeps
it live while any of its members is published. The stored remote.handle stays the one the chat was
created with, because the chat's model route names it; a record with no home, or no name of its
own, and a chat that recorded neither, keep the plain per-handle rule.

Tests: core/test/remote-chat.test.js gains four #2797 cases — one room published once per member is
one row, a changed member handle keeps the chat and never marks it gone, a line from another member
lands in that one chat, and two names at one home or one name at two homes stay two rows.
@Birfy Birfy added the agent-pr Pull request opened by an agent label Nov 10, 2026
@Birfy Birfy self-assigned this Nov 10, 2026
@Birfy Birfy added bug Something isn't working and removed bug Something isn't working labels Nov 10, 2026
@Birfy

Birfy commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 1b2b39c
Written-by: Reviewer
Attestation: 1b2b39c role=Reviewer

Ran at this tip:

$ TZ=UTC node --import ./test/harness-resolve.mjs --test --test-timeout=120000 test/remote-chat.test.js test/remote-room.test.js test/friend-chat-awaiting.test.js test/new-chat.test.js test/mention-style.test.js
# tests 163  # pass 163  # fail 0
$ TZ=UTC node --import ./test/harness-resolve.mjs --test --test-concurrency=4 --test-timeout=120000 "test/*.test.js"
# tests 2624  # pass 2525  # fail 97
$ same full run on a clean extract of base 5b8d980fd15771c20e8464cc0711b2c543be8694
# tests 2620  # pass 2521  # fail 97  (the two failure lists are identical, 97 of 97, no tip-only name)
$ cp the base blob a9dd1d853f1ab10dbde955a3ce3c2316fc9b264d over core/src/host/remote-chat.js in the tip tree, then TZ=UTC node --import ./test/harness-resolve.mjs --test --test-timeout=120000 test/remote-chat.test.js
# tests 25  # pass 22  # fail 3
$ node scripts/ds-bot-patches.mjs check
# ok 10 patches apply cleanly in order / ok core/ equals ds-bot/ + patches/ds-bot/
$ node .github/scripts/changelog.mjs check
# changelog: 22 fragment(s) in changelog.d/ are well formed
$ git hash-object core/src/host/remote-chat.js core/test/remote-chat.test.js
# fdc30cbac16e514aa3166fda64c77e47da49f22a / 1f789bbf3b404ed8c40bc028862c0a0412c46ccf  (the blobs the PR diff names)

Not run: npm --prefix plugins/* test and the .github/scripts/policy suites here (CI ran them at this tip and they are green — plugins (social|tree|timeline|workflow), policy-sign, judge, tests-ok all success); node .github/scripts/enqueue-rules.mjs (the guard check on this tip already reports one reason only); dev/bench.sh smoke (needs a harness checkout and git).

Where the duplicate was — list layer, not publication. Before this change core/src/host/remote-chat.js keyed a remote chat by the handle it arrived on: chatOfHandle compares normRemoteHandle(bot.remote.handle) against normRemoteHandle(handle) (open(), line 232 at base) and sync() decided gone from the same handle set. The publisher names one room once per member — same name (the room title) at the same home, a different member handle each — so one room opened one chat per member, which is the owner's 8+ identical rows; and when the handle carrying the room changed, sync() marked the old chat gone and opened a new one, so the count only grew. The change adds roomKeyOf (published name at that home, name cut at 40 the way a chat's stored name is cut) and: open() falls back to chatOfRoom({ home, name, plugin }) when no chat holds the handle (core/src/host/remote-chat.js:254), sync() opens each room once (:290-294) and keeps it live while any of its members is still published (:301). The stored remote.handle is left alone on purpose — the model route flybot-remote/<handle> names it.

Not a UI fold. No client or list-rendering file is in the diff (compare 5b8d980…1b2b39c4 is 1 commit, 6 files); list() keeps returning every chat record unchanged, and nothing is hidden or merged at render time. Two names at one home, and one name at two homes, stay two rows — the PR's own control case proves the rule never folds a list, and it passes on base as well as at the tip.

Red first, on base. Putting the base blob of core/src/host/remote-chat.js back under this tip's test file turns exactly the three #2797 cases red — one room 4 !== 1, a changed member handle 2 !== 1 with the old copy gone, and deliver landing on 'b2' !== 'b1' — while the control case stays green. The three assertions fail before the fix and pass after it, so the tests bite.

User-visible change has its fragment. changelog.d/2797-remote-room-one-row.fixed.md is in the diff, one line, names #2797, and changelog.mjs check accepts it.

Scope and shape. The six changed files are exactly the fix, its tests, its docs line, the changelog fragment, and the patches/ds-bot/files/ mirrors — and the mirror blobs equal the core/ blobs (fdc30cba, 1f789bbf), which ds-bot-patches check confirms both ways. Nothing under policy/, .github/scripts/, .github/workflows/, no hold, no other PR.

CI at this tip. 22 check runs; every one success or skipped (live · real model, window check are skipped by design), except enqueue-guard — which is red for exactly one reason, no Verifier report names the tip 1b2b39c446ab, i.e. this verdict was missing. That is its only objection.

The full-suite 97 failures are this box, not the diff: with no esbuild / jsdom devDeps installed and no git binary, the base tree fails the same 97 tests (identical names, set difference empty both ways).

Body metadata is in order: Owner: Chief, Reviewer: Reviewer (not the same name), label agent-pr, and the ## Checkpoint block is 1158 characters with a 548-character summary naming this tip.

@github-actions github-actions Bot added the pass label Nov 10, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit 61e4403 Nov 10, 2026
21 of 22 checks passed
@Birfy
Birfy deleted the dev/2797-remote-room-one-row branch November 10, 2026 09:49
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

远程聊天列表里同一个群重复显示很多行(同名多条,疑似按成员发布/ handle 未归一)

1 participant