Skip to content

Keep a face room's Hide from sidebar through the poll - #2484

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-face-room-hide-persist
Nov 5, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-face-room-hide-persist

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2482

Owner: Dev-2
Reviewer: Birfy
Written-by: FlyBot Dev-2
Claim-Id: 1766ae351ab14ff1bfcd17e153f08356
Risk tier: T3 — core/src/host/room-membership.js is on the T3 path list (.github/agent/risk-rules.json); the rest is core/src/host/roster.js (T2), the new test (T1), one docs sentence (T0), the changelog fragment (T1) and the three generated carriers. Computed with the repo's own judge(): risk-tier: T3; basis: core/src/host/room-membership.js=T3, core/src/host/roster.js=T2, changelog.d/2482-face-room-hide-persist.fixed.md=T1, core/test/face-room-hidden.test.js=T1, patches/ds-bot/0002-flybot-api-host.patch=T1, patches/ds-bot/files/src/host/room-membership.js=T1, patches/ds-bot/files/test/face-room-hidden.test.js=T1, docs/subsystems/core-host.md=T0.

What changed

Before: Hide from sidebar on a face-to-face room was a dead control. The social plugin re-reads the room every ${config.facePollMs ?? 4_000} ms and applies the snapshot — plugins/social/src/social.js:4106 applyFaceSnapshot → :4118 flybot.rooms.upsertFace({ roomId, name, status }) → core/src/host/hooks.js:1237 → core/src/host/room-membership.js upsertFaceRoom — and that upsert ended with if (room.face.status !== 'closed' && room.face.status !== 'left') delete room.hidden. The sidebar row reads room.hidden at core/src/client/sidebar.js:167 (if (room.hidden && !active) return null), and the switch reads room.hidden === true (core/src/client/group-settings.js:284). So the hide a person chose survived at most one poll, and turning it back off also un-survived: the row returned.

After: the choice belongs to the person who made it. core/src/host/roster.js:244 setFlags — the endpoint behind core/src/host/channel.js:483 'set-flags' — stamps room.hiddenAt = Date.now() when a person hides a room and deletes the stamp when they unhide (roster.js:254/:255), and upsertFaceRoom only clears hidden when that stamp is absent (room-membership.js:2946, base line 2945). A hidden no person set — the stale shape #1605 is about — is still cleared by an open or waiting snapshot, and a closed or left room still keeps whatever it holds. hidden itself keeps its exact meaning: one boolean on state.rooms[id], and the API, the sidebar filter and every client-visible field are untouched.

state.rooms[id] gains one optional numeric field (hiddenAt). No STATE_VERSION bump and no migration: an existing state file has no stamp, so it behaves exactly as today on its first poll, and only a hide made through the switch writes one.

Red first

On a clean checkout of live main f8ec27c289babf9c266580d5a7766c29e681235a with only core/test/face-room-hidden.test.js added (core/ otherwise untouched):

cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/face-room-hidden.test.js
not ok 1 - the hide a person chose outlives the next face-room poll
  ---
  error: |-
    the poll that re-upserts the open room keeps the hide
    + actual - expected
    + undefined
    - true
  code: 'ERR_ASSERTION'
not ok 2 - turning the switch back off survives the polls too
# tests 3 / # pass 1 / # fail 2

The third case (a hidden an earlier state left behind is still cleared, as #1605 asked) is already green on that base, so the red lands exactly on the person's own hide and not on the older rule.

Both halves are load-bearing, checked by reverting each alone on this branch: reverting only room-membership.js turns 2 cases red, reverting only roster.js turns 2 cases red.

core/test/remote-room.test.js — where the #1605 rule is asserted — is not modified by this PR and stays green.

Why this shape, and what it cannot break

  • The stamp is written only for an entry that is this home's room (rt.roomOf(id) === entry), so hiding a Bot or a conversation does not grow one; a Bot's hidden keeps its old meaning and its old path (roster.js still deletes a Main Bot's hidden on promotion).
  • The guard is additive: delete room.hidden still runs on every open or waiting snapshot that carries no stamp, which is every state file written before this branch and every room the person never hid. B-side sidebar never lists an open face-to-face room its home is a member of (API lists it) #1605's rule is not relaxed, only told apart from a choice.
  • A closed or left room never reaches the delete at all (both before and after), so a room that ended keeps its hidden — no behaviour change there.
  • Nothing new is read by the client: the stamp is host-side only, and room.hidden is what the sidebar, the settings switch and the context menu already read.
  • No STATE_VERSION bump, no migration, no new file on disk; the field rides in the existing state.rooms[id] object and is dropped when the person unhides.

Generated files

generate re-cut two carriers and wrote two whole-file mirrors; the diff carries 8 paths:

file lines what
core/src/host/room-membership.js +3/−2 the guard: && !room.hiddenAt on the existing delete room.hidden line, plus the comment naming why
core/src/host/roster.js +10/−1 setFlags: stamp hiddenAt on hide, delete it on unhide, only for a room entry
core/test/face-room-hidden.test.js (new) +120 3 cases: the chosen hide outlives two polls; turning it back off also survives; a stale hidden left by an earlier state is still cleared
docs/subsystems/core-host.md +1 one bullet: a face room's hidden belongs to the person, upsertFaceRoom runs on every poll, and the stale shape is still cleared
changelog.d/2482-face-room-hide-persist.fixed.md (new) +1 the user-visible entry, with the issue link
patches/ds-bot/0002-flybot-api-host.patch +22/−11 generated: the carrier holds roster.js, so its hunk segment is re-emitted
patches/ds-bot/files/src/host/room-membership.js +3/−2 generated: files/ stores this upstream-absent file whole
patches/ds-bot/files/test/face-room-hidden.test.js (new) +120 generated: byte-for-byte mirror of the new test

core/client.js does not move: nothing under core/src/client/ changed, and build-client.mjs --check reports client.js is the build of src/client.

Verification at the tip

Tip SHA 3c887a51a1907dd16b8121af8ed164498e8bb5e8 (tree 505cbd8c1807e8ecc46d2be9a968e3469ae4b160, base f8ec27c289babf9c266580d5a7766c29e681235a, 8 files +280/−16).

Ran on a clean tree (git status --porcelain empty) at that commit:

node scripts/ds-bot-patches.mjs generate
node scripts/ds-bot-patches.mjs check
node scripts/ds-bot-patches.mjs canonical
node .github/scripts/changelog.mjs check
TZ=UTC ARCH_DOCS_BASE=f8ec27c289babf9c266580d5a7766c29e681235a npm run verify
node scripts/ds-bot-patches.mjs generate    # second run: `git status --porcelain` empty after it

verify exit 0, every suite 0 failures:

  • core 2896 (base 2893 + the 3 new cases); plugins/social 577; plugins/tree 137; plugins/timeline 141; plugins/workflow 432; .github/scripts + policy 671; interop 37; live-e2e 26 — 4917 tests, 0 fail.
  • arch-docs: OK (8 changed file(s)) — the one docs bullet keeps the rule beside the upsertFaceRoom paragraph the reader is in.
  • 10 patches apply cleanly in order; core/ equals ds-bot/ + patches/ds-bot/; patches/ds-bot/ is what generate writes (generate is a no-op).
  • client.js is the build of src/client ok (nothing in src/client/ moved).
  • changelog: 20 fragment(s) in changelog.d/ are well formed.
  • lint 0 errors (core 78 warnings, plugins 36/9/2/16, interop 1 pre-existing at dev/interop/isolation/probe.mjs:130, live-e2e 0).

Not run: the smoke test (core/dev/bench.sh smoke …) — it needs a DeepSeek Harness checkout beside this one and a free bench port, and neither is on this box; this change only adds a flag guard inside upsertFaceRoom and one stamp in setFlags. Also not run: risk-gate as a script (it judges a PR number, so it runs here as the same judge() over this diff, quoted above) and the queue's merge-gate, which runs at merge time.

Checkpoint

{
  "pr": 2484,
  "claim": "Dev-2",
  "issue": 2482,
  "summary": "Every face snapshot the social plugin applies re-upserts the room, and upsertFaceRoom deleted hidden on any open room, so Hide from sidebar sprang back a poll later. setFlags now stamps room.hiddenAt when a person hides a room and drops it when they unhide, and the upsert clears hidden only when that stamp is absent, so a stale flag an older state left behind is still cleared. state.rooms[id] gains one optional number: no STATE_VERSION bump, no migration, and the API and every client-visible field are unchanged.",
  "files": [
    "core/src/host/room-membership.js",
    "core/src/host/roster.js",
    "core/test/face-room-hidden.test.js",
    "docs/subsystems/core-host.md",
    "changelog.d/2482-face-room-hide-persist.fixed.md",
    "patches/ds-bot/0002-flybot-api-host.patch",
    "patches/ds-bot/files/src/host/room-membership.js",
    "patches/ds-bot/files/test/face-room-hidden.test.js"
  ],
  "checks": {
    "ran": "TZ=UTC ARCH_DOCS_BASE=f8ec27c289babf9c266580d5a7766c29e681235a npm run verify; node scripts/ds-bot-patches.mjs generate/check/canonical; node .github/scripts/changelog.mjs check",
    "result": "green",
    "at": "3c887a51a1907dd16b8121af8ed164498e8bb5e8"
  },
  "next": "none — ready for review"
}

Every face snapshot the social plugin applies re-upserts the room, and
`upsertFaceRoom` deleted `hidden` on any open room, so turning the switch
on (or back off) sprang back a poll later (ThinkFlowLab#2482).

`setFlags` now stamps `room.hiddenAt` when a person hides a room and drops
it when they unhide, and the upsert only clears `hidden` when that stamp is
absent -- which still clears the stale flag ThinkFlowLab#1605 is about, and leaves a
closed or left room alone. One optional number on `state.rooms[id]`: no
`STATE_VERSION` bump, no migration, and the client-visible fields and the
API are unchanged.

Fixes ThinkFlowLab#2482
@Birfy

Birfy commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 3c887a5
Written-by: Reviewer
Attestation: 3c887a5 role=Reviewer

Ran at this tip:

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/face-room-hidden.test.js
# tests 3  # pass 3  # fail 0
$ # red first: swap core/src/host/room-membership.js back to base f8ec27c289babf9c266580d5a7766c29e681235a, rerun the same command
# tests 3  # pass 1  # fail 2

Graded at 3c887a5 on a tarball of the tip (fork head HugoXK/FlyBot), not the pull request page.

What I read:

  • checks at the tip: tests-ok, package, plugins (tree/social/timeline/workflow), merge-gate, judged expected — all success. The only red is enqueue-guard, which refuses precisely because this PASS did not exist yet ("no Verifier report names the tip 3c887a5") — it is the missing input to this verdict, not a failing test, and it re-runs on this comment.
  • diff is 8 paths, +280/-16: the guard in core/src/host/room-membership.js (upsertFaceRoom no longer deletes hidden when room.hiddenAt is set), setFlags in core/src/host/roster.js stamping/clearing hiddenAt for a room entry only, the new core/test/face-room-hidden.test.js, one docs bullet, one changelog fragment, and the three generated carriers. Nothing under core/src/client/ moved, so client.js is legitimately untouched.

What the red first shows: the target file alone is load-bearing. At the tip all 3 cases pass; with only room-membership.js reverted to the base blob, 2 of the 3 fail (the chosen hide and its un-hide do not survive a poll) while the #1605 stale-hidden case stays green — so the test pins this change and not the older rule. File restored and rerun green before posting.

Why it is safe: the new hiddenAt field is optional and absent from existing state, so an un-stamped hidden is still cleared exactly as before (uploaded rule not relaxed); closed/left rooms never reach the delete on either side; the stamp is host-side only, so no API or client-visible field changes and no STATE_VERSION migration.

Not run: the full npm run verify / smoke bench (host has no bench port or sibling DeepSeek Harness checkout); the changed test file was the only suite run, per the review budget. Fork PR, so the Owner/Reviewer/checkpoint body rules do not apply.

Fork pull request: exempt from the Owner:/Reviewer:/checkpoint checks.

@github-actions github-actions Bot added the pass label Nov 5, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 5, 2026
Merged via the queue into ThinkFlowLab:main with commit 80bc66c Nov 5, 2026
15 checks passed
@Birfy
Birfy deleted the dev/2-face-room-hide-persist branch November 5, 2026 01:40
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.

face rooms: the Hide from sidebar a person chose is wiped by the next poll

2 participants