From 3850867d3b63c4d40ff423404e27136d66af760f Mon Sep 17 00:00:00 2001 From: Day Dreamer <87640074+HugoXK@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:17:44 +0800 Subject: [PATCH] Prune the waiting-notice ledger when a hold leaves settings.approvalNotices gained one persisted entry per hold ever seen and nothing removed them, so the per-server settings file grew for the life of the install. Its only job is one waiting notice per pending hold, so a sync now drops the entry of every hold that is no longer pending. --- .../social-approval-notice-ledger.fixed.md | 1 + docs/subsystems/social.md | 2 +- plugins/social/src/social.js | 9 +++++++++ plugins/social/test/social.test.js | 19 +++++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 changelog.d/social-approval-notice-ledger.fixed.md diff --git a/changelog.d/social-approval-notice-ledger.fixed.md b/changelog.d/social-approval-notice-ledger.fixed.md new file mode 100644 index 000000000..b195dc128 --- /dev/null +++ b/changelog.d/social-approval-notice-ledger.fixed.md @@ -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. diff --git a/docs/subsystems/social.md b/docs/subsystems/social.md index 1131962dc..45dfbc0cd 100644 --- a/docs/subsystems/social.md +++ b/docs/subsystems/social.md @@ -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) diff --git a/plugins/social/src/social.js b/plugins/social/src/social.js index 68092b408..12fd6ce65 100644 --- a/plugins/social/src/social.js +++ b/plugins/social/src/social.js @@ -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). diff --git a/plugins/social/test/social.test.js b/plugins/social/test/social.test.js index 3567a8e80..ffc419082 100644 --- a/plugins/social/test/social.test.js +++ b/plugins/social/test/social.test.js @@ -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)