Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/social-approval-notice-ledger.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Bot Social settings file no longer grows forever with one entry per approval hold ever seen: a decided, rejected or gone hold drops its waiting-notice entry, so the ledger stays as small as the pending queue.
2 changes: 1 addition & 1 deletion docs/subsystems/social.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pins the list. The authority for the list is Bot Social's README and `docs/FACE_
### Inline approvals and compact Friends' Bots (#114, #115)

- A held send is never without a place (#371): the approval carries the Bot Social identity that wrote it (`approvalView` → `botId`; `pendingApprovals` maps it to the local Bot as `fromId`) and Bot Social's DLP categories (`holdReasons`: `commitment`, `pii.phone`, `pii.email`, `money`, `owner.sensitive_term`, `calendar.details`; the plain "all outbound needs approval" reason is not one). When the friend's chat is hidden, gone, home-level or not in the roster (`listedFriendChat`), the sender Bot's own chat draws the card; a listed friend chat alone shows it (#278). The card's second line reads `Held for your approval: contains commitment language` (host `t`, so zh in a zh UI) and the held text stays on the card, the bubble says only that it waits. The sender Bot's row also takes the wait dot (the `chatApprovals` provider answers for local Bots by `fromId`). No message is posted into a Bot's chat for this: it would reach the Bot's model.
- Held Bot Social drafts (`social/status`, `social/decide-approval`) and matching local `flybot/approvals` holds show as a compact in-chat card (`flybot.session.dock`, max-width ~460px, right-aligned like a bubble — not full chat width; a Bot's held reply to a friend has its card on that reply's bubble instead, invariant 8): **Waiting for approval**, one `Name → Name` line (real names, never raw `@handle` / `bot_01…`), a 3-line clamped preview with **Show more**, and **Approve** / **Reject** buttons plus a quiet **Always approve** link on one row for the owner (#260). **Always approve** is owner-only: it approves this card and turns on the existing Auto-send for that pair (per-friend `social/auto-accept`, or in a face room the writing Bot's own `social/face-auto` switch, #457) so later messages on that pair are not held; it does not decide a different sender/recipient hold and is off by default. A toast says **Auto-send on for X → Y. Change it anytime in settings.** **Approve all** sits above the cards when several are waiting; anyone else sees **Waiting for the owner's approval** with no buttons. The owner does not get a waiting bubble in their 1:1; composer holds are labelled **You**. New friends start with **Auto-send** off. The owner's click is consent (`confirm_code` in the same `decide_approval` / `set_auto_approve` call). After a decision the card reads Approved or Rejected. The social strip "N to approve" block is a one-line jump list (chat name + truncated text, no explainer, no wrapping one word per line). Auto-send of face-room posts is one compact checkbox per Bot of mine on the open room's overview (#457) — not on each pending row or room card, and not duplicated on a Face-to-face rooms header.
- Held Bot Social drafts (`social/status`, `social/decide-approval`) and matching local `flybot/approvals` holds show as a compact in-chat card (`flybot.session.dock`, max-width ~460px, right-aligned like a bubble — not full chat width; a Bot's held reply to a friend has its card on that reply's bubble instead, invariant 8): **Waiting for approval**, one `Name → Name` line (real names, never raw `@handle` / `bot_01…`), a 3-line clamped preview with **Show more**, and **Approve** / **Reject** buttons plus a quiet **Always approve** link on one row for the owner (#260). **Always approve** is owner-only: it approves this card and turns on the existing Auto-send for that pair (per-friend `social/auto-accept`, or in a face room the writing Bot's own `social/face-auto` switch, #457) so later messages on that pair are not held; it does not decide a different sender/recipient hold and is off by default. A toast says **Auto-send on for X → Y. Change it anytime in settings.** **Approve all** sits above the cards when several are waiting; anyone else sees **Waiting for the owner's approval** with no buttons. The owner does not get a waiting bubble in their 1:1; composer holds are labelled **You**. New friends start with **Auto-send** off. The owner's click is consent (`confirm_code` in the same `decide_approval` / `set_auto_approve` call). After a decision the card reads Approved or Rejected. The social strip "N to approve" block is a one-line jump list (chat name + truncated text, no explainer, no wrapping one word per line). Auto-send of face-room posts is one compact checkbox per Bot of mine on the open room's overview (#457) — not on each pending row or room card, and not duplicated on a Face-to-face rooms header. The "Waiting for the owner's approval" ledger (`settings.approvalNotices`) keeps one entry per still-pending hold and drops a hold's entry as soon as it leaves the pending queue, so the persisted settings file does not grow with every hold the install ever had.
- Live face-to-face rooms and friends' Bots sit in the core `TeamSidebar` list (globe / people mark), not a second **Friends' Bots** section. Closed, left, and ended matching rooms sit under **Past rooms**; the whole row opens the room (#304). The list does not say "The number is no longer valid." 1:1 friend chats get the same wait badge as rooms (`flybot.chatApprovals.setProvider` → `bot.pendingCount`). The social strip is **N to approve**, the + panels, and Past rooms — no **+ Add**, no per-friend groups, no Copy-handle row. **Add friend** and **Face-to-face room** are `flybot.new-chat.action` rows on the sidebar **+**. Add friend opens the join panel (handle, invite link, or code). Per-friend **Auto-send** and **Remove this Bot** live on that friend's Bot settings (`flybot.bot.details`).

### Face-to-face waiting grid, leave, and social + (#124, #125, #126)
Expand Down
9 changes: 9 additions & 0 deletions plugins/social/src/social.js
Original file line number Diff line number Diff line change
Expand Up @@ -2438,7 +2438,16 @@ export function createSocial(service, config = {}, { client: given, api: givenAp
}
async function syncApprovalNotices(items) {
settings.approvalNotices ??= {}
// The ledger keeps one waiting notice per hold and nothing else: a hold that is no longer
// pending (decided, rejected, gone) drops its entry, so the persisted map stays as small as
// the pending queue instead of growing for the life of the install.
const pending = new Set((items ?? []).map(item => item?.id).filter(Boolean))
let changed = false
for (const id of Object.keys(settings.approvalNotices)) {
if (pending.has(id)) continue
delete settings.approvalNotices[id]
changed = true
}
for (const item of items ?? []) {
if (settings.approvalNotices[item.id]) continue
// The owner of a held room line sees its Approve card, never "waiting for the owner" (#179).
Expand Down
19 changes: 19 additions & 0 deletions plugins/social/test/social.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1417,6 +1417,25 @@ test('decide-approval (#69): owner click approves or rejects via Bot Social', as
assert.equal(server.calls.some(c => c.name === 'set_auto_approve'), false)
})

test('the waiting-notice ledger drops a hold once it is no longer pending', async (t) => {
const { ctx, home, stop } = await team()
t.after(stop)
const server = mockServer()
let queue = [heldOutbound]
server.tools.list_approvals = () => ({ approvals: queue })
const settingsPath = join(home, 'settings-notices.json')
const social = createSocial(ctx.flybot, { autoAccept: false, inboxPollMs: 0, settingsPath }, { client: createMcpClient({ server: 'https://s', token: 'owner-token', fetch: server.fetch }), home: { id: 'h1', handle: '@danyang.home' } })
t.after(() => social.dispose())
await grantTeam(ctx.flybot)
await social.registerAll()
const notices = () => JSON.parse(readFileSync(settingsPath, 'utf8')).approvalNotices ?? {}
await social.pendingApprovals({ fresh: true })
assert.deepEqual(Object.keys(notices()), ['apr-hold-1'], 'one entry per pending hold')
queue = []
await social.pendingApprovals({ fresh: true })
assert.deepEqual(notices(), {}, 'a hold that left the pending queue leaves no ledger entry behind')
})

test('#417: Auto-send from now on turns on Auto-send for that pair only: my Bot → that person', async (t) => {
const { ctx, call, stop } = await team()
t.after(stop)
Expand Down
Loading