feat(v2): track unregistered senders + setup improvements
- Add unregistered_senders table to capture dropped message origins (one row per sender, upserted with message_count and last_seen) - Add inbound DM logging to chat-sdk-bridge for debugging - Add vercel CLI to base container image - Install vercel-cli and frontend-engineer container skills - Default requiresTrigger to false in register step Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -178,6 +178,7 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter
|
||||
// DMs — always forward + subscribe
|
||||
chat.onDirectMessage(async (thread, message) => {
|
||||
const channelId = adapter.channelIdFromThreadId(thread.id);
|
||||
log.info('Inbound DM received', { adapter: adapter.name, channelId, sender: (message.author as any)?.fullName ?? (message.author as any)?.userId ?? 'unknown', threadId: thread.id });
|
||||
await setupConfig.onInbound(channelId, null, await messageToInbound(message));
|
||||
await thread.subscribe();
|
||||
});
|
||||
|
||||
44
src/db/dropped-messages.ts
Normal file
44
src/db/dropped-messages.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { getDb } from './connection.js';
|
||||
|
||||
export interface UnregisteredSender {
|
||||
channel_type: string;
|
||||
platform_id: string;
|
||||
user_id: string | null;
|
||||
sender_name: string | null;
|
||||
reason: string;
|
||||
messaging_group_id: string | null;
|
||||
agent_group_id: string | null;
|
||||
message_count: number;
|
||||
first_seen: string;
|
||||
last_seen: string;
|
||||
}
|
||||
|
||||
export function recordDroppedMessage(msg: {
|
||||
channel_type: string;
|
||||
platform_id: string;
|
||||
user_id: string | null;
|
||||
sender_name: string | null;
|
||||
reason: string;
|
||||
messaging_group_id: string | null;
|
||||
agent_group_id: string | null;
|
||||
}): void {
|
||||
const now = new Date().toISOString();
|
||||
getDb()
|
||||
.prepare(
|
||||
`INSERT INTO unregistered_senders (channel_type, platform_id, user_id, sender_name, reason, messaging_group_id, agent_group_id, message_count, first_seen, last_seen)
|
||||
VALUES (@channel_type, @platform_id, @user_id, @sender_name, @reason, @messaging_group_id, @agent_group_id, 1, @now, @now)
|
||||
ON CONFLICT (channel_type, platform_id) DO UPDATE SET
|
||||
user_id = COALESCE(excluded.user_id, unregistered_senders.user_id),
|
||||
sender_name = COALESCE(excluded.sender_name, unregistered_senders.sender_name),
|
||||
reason = excluded.reason,
|
||||
message_count = unregistered_senders.message_count + 1,
|
||||
last_seen = excluded.last_seen`,
|
||||
)
|
||||
.run({ ...msg, now });
|
||||
}
|
||||
|
||||
export function getUnregisteredSenders(limit = 50): UnregisteredSender[] {
|
||||
return getDb()
|
||||
.prepare('SELECT * FROM unregistered_senders ORDER BY last_seen DESC LIMIT ?')
|
||||
.all(limit) as UnregisteredSender[];
|
||||
}
|
||||
27
src/db/migrations/008-dropped-messages.ts
Normal file
27
src/db/migrations/008-dropped-messages.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
import type { Migration } from './index.js';
|
||||
|
||||
export const migration008: Migration = {
|
||||
version: 8,
|
||||
name: 'dropped-messages',
|
||||
up: (db: Database.Database) => {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS unregistered_senders (
|
||||
channel_type TEXT NOT NULL,
|
||||
platform_id TEXT NOT NULL,
|
||||
user_id TEXT,
|
||||
sender_name TEXT,
|
||||
reason TEXT NOT NULL,
|
||||
messaging_group_id TEXT,
|
||||
agent_group_id TEXT,
|
||||
message_count INTEGER NOT NULL DEFAULT 1,
|
||||
first_seen TEXT NOT NULL,
|
||||
last_seen TEXT NOT NULL,
|
||||
PRIMARY KEY (channel_type, platform_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_unregistered_senders_last_seen
|
||||
ON unregistered_senders(last_seen);
|
||||
`);
|
||||
},
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { migration003 } from './003-pending-approvals.js';
|
||||
import { migration004 } from './004-agent-destinations.js';
|
||||
import { migration005 } from './005-pending-credentials.js';
|
||||
import { migration007 } from './007-pending-approvals-title-options.js';
|
||||
import { migration008 } from './008-dropped-messages.js';
|
||||
|
||||
export interface Migration {
|
||||
version: number;
|
||||
@@ -14,7 +15,7 @@ export interface Migration {
|
||||
up: (db: Database.Database) => void;
|
||||
}
|
||||
|
||||
const migrations: Migration[] = [migration001, migration002, migration003, migration004, migration005, migration007];
|
||||
const migrations: Migration[] = [migration001, migration002, migration003, migration004, migration005, migration007, migration008];
|
||||
|
||||
export function runMigrations(db: Database.Database): void {
|
||||
db.exec(`
|
||||
|
||||
@@ -26,6 +26,7 @@ import { log } from './log.js';
|
||||
import { resolveSession, writeSessionMessage } from './session-manager.js';
|
||||
import { wakeContainer } from './container-runner.js';
|
||||
import { getSession } from './db/sessions.js';
|
||||
import { recordDroppedMessage } from './db/dropped-messages.js';
|
||||
import type { MessagingGroup, MessagingGroupAgent } from './types.js';
|
||||
|
||||
function generateId(): string {
|
||||
@@ -94,6 +95,16 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
|
||||
channelType: event.channelType,
|
||||
platformId: event.platformId,
|
||||
});
|
||||
const parsed = safeParseContent(event.message.content);
|
||||
recordDroppedMessage({
|
||||
channel_type: event.channelType,
|
||||
platform_id: event.platformId,
|
||||
user_id: parsed.senderId ?? null,
|
||||
sender_name: parsed.sender ?? null,
|
||||
reason: 'no_agent_wired',
|
||||
messaging_group_id: mg.id,
|
||||
agent_group_id: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,6 +115,16 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
|
||||
messagingGroupId: mg.id,
|
||||
channelType: event.channelType,
|
||||
});
|
||||
const parsed = safeParseContent(event.message.content);
|
||||
recordDroppedMessage({
|
||||
channel_type: event.channelType,
|
||||
platform_id: event.platformId,
|
||||
user_id: parsed.senderId ?? null,
|
||||
sender_name: parsed.sender ?? null,
|
||||
reason: 'no_trigger_match',
|
||||
messaging_group_id: mg.id,
|
||||
agent_group_id: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -111,7 +132,7 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
|
||||
if (mg.unknown_sender_policy !== 'public') {
|
||||
const gate = enforceAccess(userId, match.agent_group_id);
|
||||
if (!gate.allowed) {
|
||||
handleUnknownSender(mg, userId, match.agent_group_id, gate.reason);
|
||||
handleUnknownSender(mg, userId, match.agent_group_id, gate.reason, event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -239,7 +260,19 @@ function handleUnknownSender(
|
||||
userId: string | null,
|
||||
agentGroupId: string,
|
||||
accessReason: string,
|
||||
event: InboundEvent,
|
||||
): void {
|
||||
const parsed = safeParseContent(event.message.content);
|
||||
const dropRecord = {
|
||||
channel_type: event.channelType,
|
||||
platform_id: event.platformId,
|
||||
user_id: userId,
|
||||
sender_name: parsed.sender ?? null,
|
||||
reason: `unknown_sender_${mg.unknown_sender_policy}`,
|
||||
messaging_group_id: mg.id,
|
||||
agent_group_id: agentGroupId,
|
||||
};
|
||||
|
||||
// In 'strict' mode we just drop. In 'request_approval' mode we log and
|
||||
// queue an approval to add the sender as a member — the approval flow
|
||||
// itself is a follow-up (needs an action kind like `add_group_member`).
|
||||
@@ -250,18 +283,18 @@ function handleUnknownSender(
|
||||
userId,
|
||||
accessReason,
|
||||
});
|
||||
recordDroppedMessage(dropRecord);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mg.unknown_sender_policy === 'request_approval') {
|
||||
// Placeholder: drop for now but log as a request. Follow-up wires this
|
||||
// into the approval flow (request admin-of-group / owner to add user).
|
||||
log.info('MESSAGE DROPPED — unknown sender (approval flow TODO)', {
|
||||
messagingGroupId: mg.id,
|
||||
agentGroupId,
|
||||
userId,
|
||||
accessReason,
|
||||
});
|
||||
recordDroppedMessage(dropRecord);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -269,3 +302,12 @@ function handleUnknownSender(
|
||||
// Ensure the membership invariant isn't in an odd state.
|
||||
void isMember;
|
||||
}
|
||||
|
||||
function safeParseContent(raw: string): { text?: string; sender?: string; senderId?: string } {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return { text: raw };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user