Repository navigation
Deny a send to a friend removed since the roster was cached (#666) - #675
Conversation
The send policy resolved its target from the cached roster (30 s TTL). When a friendship was removed outside this plugin — the agent's remove_friend tool, or the Bot Social console — the roster kept the Bot, so a send in that window was held for approval, and approving it lost the line: send_as_member answered "no active relationship between these bots". The policy now asks Bot Social about the friendship itself (list_friends for the home and for the sending Bot's member identity) before it asks the owner, and denies with the usual "not a Bot a friend shared with you" reason. A two-second cache keeps a burst of lines at one call per friend; no answer at all keeps the roster's verdict, so an older server or a down link does not start refusing legitimate sends. A deny drops that friend from the cached roster and republishes, and a relationship.changed event with any status but active now invalidates the roster instead of serving the removed friend for the rest of the TTL. Refs #666
|
Verdict: INC at a7f1314 Reviewed this tip only. CI is green (9/9 1. The 2 s positive cache keeps the defect alive for the "same millisecond" case, and the new test steps over it.
That is a design choice, not a typo, so pick one and say which: (a) never cache 2. A successful answer without a
So one malformed answer turns a working friend into a denied send and makes the Bot vanish from the sidebar until the next roster refresh. Fix: treat a result that is not a real list as no answer — Medium, not blocking: this probe now sits on the send path, and Not done here and fine as stated in the PR: the two-instance selftest (one home on this machine), so the 8 ms window has no end-to-end coverage — the unit path is what needs to change above. |
|
Verdict: PASS at 5aeec75 Ran:
Not run:
Reviewed at this tip only. All 9 checks are All three findings from the INC at
Red without the source change: with Non-blocking
Risk: |
Fixes #666 — a message to a friend's Bot right after that friendship was removed is refused at once instead of being held and then lost on approve. One plugin change, its tests, its subsystem doc and one changelog fragment.
This tip answers the INC on
a7f13140with option (b): arelationship.changedevent now drops the cached friendship answer as well as the roster, and releasing a held card asks again with a fresh answer. The two new window cases are pinned at+0 msand+500 ms.Reproduce
A full
remove_friendon Bot Social, then a send inside the same second, split three ways by this plugin:plugins/social/src/social.jssendTargetresolves the target fromremote, the roster it fetched up toREMOTE_TTL_MS(30 s) ago. A friendship removed outside this plugin never reaches that cache, so the target still resolves and the policy returnshold(messages to @bob.home's Bots need your approval) instead of the deny it gives ~20 s later (is not a Bot a friend shared with you).send_as_member, and Bot Social answersno active relationship between these bots: the line is lost, with a failed record and no card that says so.relationship.changedevent, but the handler only served the roster: the send path kept its own two-second "still friends" answer, so a send at+0 msor+500 msafter the event was still held (INC, reproduced by moving only the clock).list_friendswith an envelope but nofriendsarray was read as "no friends": a real friend was denied and dropped from the list (INC).What changed
plugins/social/src/social.jsfriendshipActive(friendHome, fromBotId, { fresh }): readslist_friendsfor the home and for the sending Bot's own member identity (a per-Bot friendship, Friend's line on the sharing home: lands unmarked in the Bot's own chat with no unread badge, first line needs a reload; social/overview lists no per-Bot friendships #420, is listed on that Bot), and accepts only an entry whosestatusisactive(or absent) whose peer handle belongs to that friend's home. The answer is cached for 2 s (FRIEND_CHECK_MS), so a burst of lines costs one call per friend rather than one per line;fresh: trueskips that cache and always asks.forgetFriendStatus(homeKey): a removal, or any relationship change, drops every cached answer — all of them when the event does not name a home, that home's when a deny does.forgetFriendHomecalls it, so the roster row and the answer go together.pollOnce: arelationship.changedevent with any status butactivesetsfetchedAt = 0and callsforgetFriendStatus(); anactiveone clears the answers too, so a re-added friend is not judged on a stale deny.if (!Array.isArray(answer?.friends)) continue, so an unreadable answer keeps the roster's verdict instead of unfriending a real friend.sendOut,msg.approved === true, not a room line) asks again with{ fresh: true }and refuses with the same is not a Bot a friend shared with you copy, so an approved card fails with a reason instead of being handed to Bot Social to be refused withno active relationship.forgetFriendHome(home): a deny drops that friend from the cachedremoteandserverRosterand republishes, so the list and the picker stop offering a Bot that can no longer be reached.plugins/social/src/mcp-client.js—callTool(name, args, { timeoutMs })narrows the deadline for one call; the probe passesFRIEND_CALL_MS = 1.5 s, because it runs on the send path and must not add the client's 30 s deadline to every line while Bot Social is down.docs/subsystems/social.md— the removal window, the cache invalidation, the approve re-check, the unreadable answer and the probe deadline.changelog.d/666-unfriended-send.fixed.md— the user-visible change, including the approve-time refusal.What I ran
Tip SHA:
5aeec75410501a24dba7e45d2459f317ee036b99(Node 22.23.3,TZ=UTC, base7223a1a1):plugins/social, full suite: 324/324 pass (node --import ./test/resolve.mjs --test --test-concurrency=2 "test/*.test.js").#666tests on their own: 4/4 pass — the+0 msdeny, the+500 msdeny, the approve-time refusal, and the unreadable answer that is not a removal (the older roster-drop case is still green).node scripts/lint-plugins.mjs→ 0 errors, all three plugins pack and import, exit 0.node core/scripts/check-arch-docs.mjs→arch-docs: OK (4 changed file(s)).node .github/scripts/changelog.mjs check→ 10 fragments well formed.git diff --shortstat 7223a1a1...HEAD→ 5 files, +232/−5.Red without the source change
Against the previous tip
a7f13140(this fix reverted, the same tests kept,--test-name-pattern "#666"):#666: a removal event drops the friendship answer, so a send in the same millisecond is denied→ fail:holdinstead ofdeny— the INC's first point in its strongest form, no clock movement at all.#666: the same removal 500 ms after the event is still denied→ fail: stillholdinside the old two-second answer.#666: approving a card held before the removal is refused before Bot Social is asked→ fail: the release path callssend_as_memberand Bot Social refuses it.#666: an answer without a friends list is not a removal→ fail: a real friend is denied and disappears from the list.#666: a relationship removal event drops the cached rostercase stays green on both tips, as it should — it covers the roster half, which was already fixed.Not run
selftest/: two isolated homes, ports 3096/3097, real Bot Social): this box has only this checkout, no second home and no reachable Bot Social server, so the window was not re-measured end to end. The unit tests drive the same code paths with a fake clock and a mock server whoselist_friendsanswer is the only signal.npm run verify(it needs the core suite and the two other plugin suites; nocore/,plugins/treeorplugins/timelinefile is in the diff).tests-okat the tip covers all of them.dev/bench.shneedspnpm, which this box does not have.Risk
risk-gate.mjs(plugins/social/src/social.jsandplugins/social/src/mcp-client.jsare T3 paths; 232 changed lines in 5 files): it needs a PASS at this tip plus greentests-ok, and the gating agent merges it after that.denywith the not a Bot a friend shared with you reason instead of a hold card, and a card held before the removal fails at approval with that reason instead of losing the line. The owner sees no card to click for a line that cannot leave.list_friendscall per friend per 2 s in a run of outgoing lines, one per approval, and each probe now gives up after 1.5 s instead of waiting out the client's 30 s. Room lines, a friend's incoming line and non-Bot-Social transports skip it.