diff --git a/src/db/migrations/013-approval-render-metadata.ts b/src/db/migrations/013-approval-render-metadata.ts new file mode 100644 index 000000000..3a1af2828 --- /dev/null +++ b/src/db/migrations/013-approval-render-metadata.ts @@ -0,0 +1,27 @@ +/** + * Persist ask_question render metadata (title + options_json) on + * `pending_channel_approvals` and `pending_sender_approvals`, mirroring the + * columns migration 003 / module-approvals-title-options added to + * `pending_approvals`. + * + * Before this, `getAskQuestionRender` hardcoded the title + option labels + * for these two tables in the DB-access layer — duplicating wording that + * also lived in the approval modules and causing a visible drift between + * the initial card title ("📣 Bot mentioned in new chat" / "💬 New direct + * message", chosen per event) and the post-click render ("📣 Channel + * registration", constant). Storing the render metadata alongside the row + * lets both sides read from the same source. + */ +import type Database from 'better-sqlite3'; +import type { Migration } from './index.js'; + +export const migration013: Migration = { + version: 13, + name: 'approval-render-metadata', + up(db: Database.Database) { + db.exec(`ALTER TABLE pending_channel_approvals ADD COLUMN title TEXT NOT NULL DEFAULT ''`); + db.exec(`ALTER TABLE pending_channel_approvals ADD COLUMN options_json TEXT NOT NULL DEFAULT '[]'`); + db.exec(`ALTER TABLE pending_sender_approvals ADD COLUMN title TEXT NOT NULL DEFAULT ''`); + db.exec(`ALTER TABLE pending_sender_approvals ADD COLUMN options_json TEXT NOT NULL DEFAULT '[]'`); + }, +}; diff --git a/src/db/migrations/index.ts b/src/db/migrations/index.ts index 33e6963a9..b46e6787c 100644 --- a/src/db/migrations/index.ts +++ b/src/db/migrations/index.ts @@ -9,6 +9,7 @@ import { migration009 } from './009-drop-pending-credentials.js'; import { migration010 } from './010-engage-modes.js'; import { migration011 } from './011-pending-sender-approvals.js'; import { migration012 } from './012-channel-registration.js'; +import { migration013 } from './013-approval-render-metadata.js'; import { moduleApprovalsPendingApprovals } from './module-approvals-pending-approvals.js'; import { moduleApprovalsTitleOptions } from './module-approvals-title-options.js'; @@ -29,6 +30,7 @@ const migrations: Migration[] = [ migration010, migration011, migration012, + migration013, ]; export function runMigrations(db: Database.Database): void { diff --git a/src/db/sessions.ts b/src/db/sessions.ts index e9461ca18..5c53ad5d2 100644 --- a/src/db/sessions.ts +++ b/src/db/sessions.ts @@ -194,32 +194,20 @@ export function getAskQuestionRender( | undefined; if (a?.title) return { title: a.title, options: JSON.parse(a.options_json) }; - // Channel-registration approval — options are fixed constants. + // Channel-registration + unknown-sender approvals persist title/options_json + // the same way pending_approvals does — just SELECT and return. if (hasTable(getDb(), 'pending_channel_approvals')) { - const c = getDb().prepare('SELECT 1 FROM pending_channel_approvals WHERE messaging_group_id = ?').get(id); - if (c) { - return { - title: '📣 Channel registration', - options: [ - { label: 'Approve', selectedLabel: '✅ Wired', value: 'approve' }, - { label: 'Ignore', selectedLabel: '🙅 Ignored', value: 'reject' }, - ], - }; - } + const c = getDb() + .prepare('SELECT title, options_json FROM pending_channel_approvals WHERE messaging_group_id = ?') + .get(id) as { title: string; options_json: string } | undefined; + if (c?.title) return { title: c.title, options: JSON.parse(c.options_json) }; } - // Unknown-sender approval — options are fixed constants. if (hasTable(getDb(), 'pending_sender_approvals')) { - const s = getDb().prepare('SELECT 1 FROM pending_sender_approvals WHERE id = ?').get(id); - if (s) { - return { - title: '👤 New sender', - options: [ - { label: 'Allow', selectedLabel: '✅ Allowed', value: 'approve' }, - { label: 'Deny', selectedLabel: '❌ Denied', value: 'reject' }, - ], - }; - } + const s = getDb().prepare('SELECT title, options_json FROM pending_sender_approvals WHERE id = ?').get(id) as + | { title: string; options_json: string } + | undefined; + if (s?.title) return { title: s.title, options: JSON.parse(s.options_json) }; } return undefined; diff --git a/src/modules/permissions/channel-approval.ts b/src/modules/permissions/channel-approval.ts index e4b2142d7..8ab41bc62 100644 --- a/src/modules/permissions/channel-approval.ts +++ b/src/modules/permissions/channel-approval.ts @@ -120,6 +120,7 @@ export async function requestChannelApproval(input: RequestChannelApprovalInput) : senderName ? `${senderName} DM'd your agent on ${originChannelType}. Wire it to ${target.name} and let it respond?` : `Someone DM'd your agent on ${originChannelType}. Wire it to ${target.name} and let it respond?`; + const options = normalizeOptions(APPROVAL_OPTIONS); createPendingChannelApproval({ messaging_group_id: messagingGroupId, @@ -127,6 +128,8 @@ export async function requestChannelApproval(input: RequestChannelApprovalInput) original_message: JSON.stringify(event), approver_user_id: delivery.userId, created_at: new Date().toISOString(), + title, + options_json: JSON.stringify(options), }); const adapter = getDeliveryAdapter(); @@ -151,7 +154,7 @@ export async function requestChannelApproval(input: RequestChannelApprovalInput) questionId: messagingGroupId, title, question, - options: normalizeOptions(APPROVAL_OPTIONS), + options, }), ); log.info('Channel registration card delivered', { diff --git a/src/modules/permissions/db/pending-channel-approvals.ts b/src/modules/permissions/db/pending-channel-approvals.ts index d3e665ad2..d402074b7 100644 --- a/src/modules/permissions/db/pending-channel-approvals.ts +++ b/src/modules/permissions/db/pending-channel-approvals.ts @@ -17,6 +17,10 @@ export interface PendingChannelApproval { original_message: string; approver_user_id: string; created_at: string; + /** Card title shown at creation and re-used by getAskQuestionRender on click. */ + title: string; + /** Normalized options (JSON-encoded NormalizedOption[]) — same shape persisted on pending_approvals. */ + options_json: string; } export function createPendingChannelApproval(row: PendingChannelApproval): void { @@ -24,11 +28,11 @@ export function createPendingChannelApproval(row: PendingChannelApproval): void .prepare( `INSERT INTO pending_channel_approvals ( messaging_group_id, agent_group_id, original_message, - approver_user_id, created_at + approver_user_id, created_at, title, options_json ) VALUES ( @messaging_group_id, @agent_group_id, @original_message, - @approver_user_id, @created_at + @approver_user_id, @created_at, @title, @options_json )`, ) .run(row); diff --git a/src/modules/permissions/db/pending-sender-approvals.ts b/src/modules/permissions/db/pending-sender-approvals.ts index 77a5699af..4d32bf4fa 100644 --- a/src/modules/permissions/db/pending-sender-approvals.ts +++ b/src/modules/permissions/db/pending-sender-approvals.ts @@ -19,6 +19,10 @@ export interface PendingSenderApproval { original_message: string; approver_user_id: string; created_at: string; + /** Card title shown at creation and re-used by getAskQuestionRender on click. */ + title: string; + /** Normalized options (JSON-encoded NormalizedOption[]) — same shape persisted on pending_approvals. */ + options_json: string; } export function createPendingSenderApproval(row: PendingSenderApproval): void { @@ -26,11 +30,13 @@ export function createPendingSenderApproval(row: PendingSenderApproval): void { .prepare( `INSERT INTO pending_sender_approvals ( id, messaging_group_id, agent_group_id, sender_identity, - sender_name, original_message, approver_user_id, created_at + sender_name, original_message, approver_user_id, created_at, + title, options_json ) VALUES ( @id, @messaging_group_id, @agent_group_id, @sender_identity, - @sender_name, @original_message, @approver_user_id, @created_at + @sender_name, @original_message, @approver_user_id, @created_at, + @title, @options_json )`, ) .run(row); diff --git a/src/modules/permissions/sender-approval.ts b/src/modules/permissions/sender-approval.ts index a20e14f3e..fb3e24e01 100644 --- a/src/modules/permissions/sender-approval.ts +++ b/src/modules/permissions/sender-approval.ts @@ -92,6 +92,7 @@ export async function requestSenderApproval(input: RequestSenderApprovalInput): const title = '👤 New sender'; const question = `${senderDisplay} wants to talk to your agent in ${originName}. Allow?`; + const options = normalizeOptions(APPROVAL_OPTIONS); createPendingSenderApproval({ id: approvalId, @@ -102,6 +103,8 @@ export async function requestSenderApproval(input: RequestSenderApprovalInput): original_message: JSON.stringify(event), approver_user_id: target.userId, created_at: new Date().toISOString(), + title, + options_json: JSON.stringify(options), }); const adapter = getDeliveryAdapter(); @@ -126,7 +129,7 @@ export async function requestSenderApproval(input: RequestSenderApprovalInput): questionId: approvalId, title, question, - options: APPROVAL_OPTIONS, + options, }), ); log.info('Unknown-sender approval card delivered', {