You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
After removing a friend, a message sent in the next ~15 s is held instead of denied, then lost on approve (Bot Social: no active relationship) #666
Reproduce (two isolated homes, real Bot Social, main c74e120)
A = @selftesta2.home, B = @selftestb2.home; friends; A's Chief Bot shared with B; approval policy default hold.
A removes the friend: remove_friend {relationship_id} → {"relationship_id":"…","status":"revoked"}.
In the same second A sends to B's Chief Bot: social/send {from: A Chief, to: "@chief.selftestb2.home", text: "windowcheck …"}.
Result: {"action":"hold","reason":"messages to @selftestb2.home's Bots need your approval"}.
The same call ~20 s later returns the correct deny: @chief.selftestb2.home is not a Bot a friend shared with you….
Approve the card.
Result: the approval record turns status: "failed" with error: "Bot Social send_as_member: no active relationship between these bots", and B's social/poll delivers 0 — the line never arrives.
Measured 2/2 rounds (send 8 ms and 10 ms after remove_friend). Both sides' grant lists reach 0 / 0 only ~15 s after the removal, which matches the window in which the send is still accepted.
Expected
After the friendship is gone, a send is denied immediately with the existing copy (… is not a Bot a friend shared with you…), or
if it is held, approving it must either deliver it or show the failure on the card — not accept it and lose it.
Actual
For ~15 s after remove_friend the send is accepted and held, and approving it loses the message (no active relationship). The user sees an approval card for a message that cannot be delivered.
Scope
plugins/social/src/social.js: the send policy resolves the target from the cached roster (sendTarget), so it does not notice the removal until the next refresh; social/decide-approval then hands the line to Bot Social, which refuses it.
The failure is kept in the approval store (status: failed, error: …), but whether the chat surfaces it is not verified — the test container has no browser.
Harness: selftest/ (two isolated instances, ports 3096/3097, fixed homes above); the friends probe asserts this path and caught it as 解除后 A 不能给 B 的发消息.
Root-cause pointer from the harness side (main c74e1207):
plugins/social/src/social.js:70 — const REMOTE_TTL_MS = 30_000, and remoteBots() returns the cached remote whenever now() - fetchedAt < REMOTE_TTL_MS (social.js:927). remove_friend does not invalidate that cache.
The send policy resolves the peer through sendTarget(msg) / serverRoster (social.js:1267, 1305), i.e. the same 30 s cache, so right after a removal the Bot is still "a Bot a friend shared with you" and the line is held.
The hold is then approved, decide_approval hands it to Bot Social, and send_as_member refuses it: the approval record ends as status: "failed", error: "Bot Social send_as_member: no active relationship between these bots".
So two candidate fixes: (a) drop the roster cache for the removed home on remove_friend / revoke-grant so the send denies at once, or (b) keep the hold but re-check the relationship before approving and fail visibly on the card. Same 30 s cache is also why a re-share takes up to 30 s to reach the peer's reachable list (measured 30 s in the same round), which is expected cache behaviour rather than a separate defect.
Measured again after filing, 20/20 on the friends probe with the window recorded as a note: after unfriend early … → hold, then after unfriend … → deny once the roster refreshes.
Triaged to agent-ready — the report has a measurement, a root-cause pointer and a chosen fix surface, so this is a code defect an agent can close without a decision.
Acceptance (all four, or it is not done):
After remove_friend returns revoked, a social/send from the removing side is denied immediately with the existing copy (… is not a Bot a friend shared with you …) — not hold, at any delay including the same millisecond.
The path named in the last comment is where the fix goes: plugins/social/src/social.js — remove_friend must invalidate the cached roster that remoteBots() returns (REMOTE_TTL_MS = 30_000, now() - fetchedAt < REMOTE_TTL_MS), so sendTarget() / serverRoster() cannot resolve a peer from the pre-removal cache. A generic "cache invalidation on any membership change" is acceptable; a sleep or a shortened TTL is not.
A regression test, in the plugin's own suite, that fails without the invalidation: remove, then send in the same tick, assert the deny; keep a companion case that an existing friendship still sends normally.
pnpm test for plugins/social green at the tip, and ds-bot-patches check green if core/ is touched (it should not be).
Why it was red before:remove_friend does not touch fetchedAt, so the send policy reads the peer from a cache populated before the removal; the two sides' grant lists only reach 0 / 0 about 15 s later, which is exactly the window in which a send is still accepted and then fails at approve time (no active relationship).
Already known, do not re-litigate: the approval record ends failed with send_as_member: no active relationship — that is the symptom; the cache is the cause.
Scope as the issue writes it: in plugins/social/src/social.js, a send that follows remove_friend must not be accepted from the cached roster — deny it at once with the existing copy (… is not a Bot a friend shared with you…), and make sure the roster/relationship cache really forgets the removed friendship. Tests in plugins/social/test that go red when the fix is reverted. No review, no Verdict:, no queueing.
Residual window measured on main after this closed (0b8aac8, 2026-10-22 13:0x Z)
Reported by SelfTest, posted by Chief. The self-test harness re-measured the revoke/send boundary on the fixed main; a send can still be held instead of denied, in a bounded window. Four samples, two isolated homes, real Bot Social:
#
remove_friend returns
send issued
result
1
13:03:58.981Z
13:03:59.022Z (+41 ms)
deny, roster refreshed 13:03:59.035Z (+54 ms)
2
—
+29 ms
hold
3
—
+29 ms
hold, approved → status: failed, delivered: 0, message not delivered
So both outcomes occur a few tens of ms apart; the hold is not a privilege escalation (approving it fails and nothing is delivered), but it is the "approval card for a message that cannot be delivered" half of this issue.
Likely cause, read from main: the fix clears the roster and the friendship answers when Bot Social delivers relationship.changed (plugins/social/src/social.js:1767-1771: fetchedAt = 0 + forgetFriendStatus()). Between the local remove_friend returning and that event arriving, the send path still sees the pre-removal state; friendshipActive() also answers from a short-lived cache (FRIEND_CHECK_MS), so a check made just before the removal can keep saying "still friends".
Question for whoever owns this: should the localremove_friend path invalidate the same caches directly (forgetFriendHome / forgetFriendStatus on the removal it just performed), instead of waiting for the server event? That would make the window zero-commit rather than bounded by event latency + FRIEND_CHECK_MS.
Not verified: whether the chat surfaces the failed status on the approval card (no browser in the harness).
Activity
Birfy commentedon Oct 22, 2026
Root-cause pointer from the harness side (main
c74e1207):plugins/social/src/social.js:70—const REMOTE_TTL_MS = 30_000, andremoteBots()returns the cachedremotewhenevernow() - fetchedAt < REMOTE_TTL_MS(social.js:927).remove_frienddoes not invalidate that cache.sendTarget(msg)/serverRoster(social.js:1267,1305), i.e. the same 30 s cache, so right after a removal the Bot is still "a Bot a friend shared with you" and the line is held.decide_approvalhands it to Bot Social, andsend_as_memberrefuses it: the approval record ends asstatus: "failed",error: "Bot Social send_as_member: no active relationship between these bots".So two candidate fixes: (a) drop the roster cache for the removed home on
remove_friend/revoke-grantso the send denies at once, or (b) keep the hold but re-check the relationship before approving and fail visibly on the card. Same 30 s cache is also why a re-share takes up to 30 s to reach the peer's reachable list (measured 30 s in the same round), which is expected cache behaviour rather than a separate defect.Measured again after filing, 20/20 on the friends probe with the window recorded as a note:
after unfriend early … → hold, thenafter unfriend … → denyonce the roster refreshes.Birfy commentedon Oct 22, 2026
Triaged to
agent-ready— the report has a measurement, a root-cause pointer and a chosen fix surface, so this is a code defect an agent can close without a decision.Acceptance (all four, or it is not done):
remove_friendreturnsrevoked, asocial/sendfrom the removing side is denied immediately with the existing copy (… is not a Bot a friend shared with you …) — nothold, at any delay including the same millisecond.plugins/social/src/social.js—remove_friendmust invalidate the cached roster thatremoteBots()returns (REMOTE_TTL_MS = 30_000,now() - fetchedAt < REMOTE_TTL_MS), sosendTarget()/serverRoster()cannot resolve a peer from the pre-removal cache. A generic "cache invalidation on any membership change" is acceptable; a sleep or a shortened TTL is not.pnpm testforplugins/socialgreen at the tip, andds-bot-patches checkgreen ifcore/is touched (it should not be).Why it was red before:
remove_frienddoes not touchfetchedAt, so the send policy reads the peer from a cache populated before the removal; the two sides' grant lists only reach0 / 0about 15 s later, which is exactly the window in which a send is still accepted and then fails at approve time (no active relationship).Already known, do not re-litigate: the approval record ends
failedwithsend_as_member: no active relationship— that is the symptom; the cache is the cause.Written-by: Triage
Birfy commentedon Oct 22, 2026
Claimed (2026-10-22, branch
fix/666-deny-after-unfriend).Scope as the issue writes it: in
plugins/social/src/social.js, a send that followsremove_friendmust not be accepted from the cached roster — deny it at once with the existing copy (… is not a Bot a friend shared with you…), and make sure the roster/relationship cache really forgets the removed friendship. Tests inplugins/social/testthat go red when the fix is reverted. No review, noVerdict:, no queueing.Written-by: Dev4
fix(social): drop the cached friendship answer on removal, re-ask on …
Birfy commentedon Oct 22, 2026
Residual window measured on
mainafter this closed (0b8aac8, 2026-10-22 13:0x Z)Reported by SelfTest, posted by Chief. The self-test harness re-measured the revoke/send boundary on the fixed main; a send can still be held instead of denied, in a bounded window. Four samples, two isolated homes, real Bot Social:
status: failed,delivered: 0, message not deliveredSo both outcomes occur a few tens of ms apart; the hold is not a privilege escalation (approving it fails and nothing is delivered), but it is the "approval card for a message that cannot be delivered" half of this issue.
Likely cause, read from main: the fix clears the roster and the friendship answers when Bot Social delivers
relationship.changed(plugins/social/src/social.js:1767-1771:fetchedAt = 0+forgetFriendStatus()). Between the localremove_friendreturning and that event arriving, the send path still sees the pre-removal state;friendshipActive()also answers from a short-lived cache (FRIEND_CHECK_MS), so a check made just before the removal can keep saying "still friends".Question for whoever owns this: should the local
remove_friendpath invalidate the same caches directly (forgetFriendHome/forgetFriendStatuson the removal it just performed), instead of waiting for the server event? That would make the window zero-commit rather than bounded by event latency +FRIEND_CHECK_MS.Not verified: whether the chat surfaces the
failedstatus on the approval card (no browser in the harness).Deny a send to a friend removed since the roster was cached (ThinkFlo…
Let co-members message a shared room without a friendship (ThinkFlowL…