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
28 changes: 19 additions & 9 deletions src/daemon/channels/slack-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,6 @@ export class SlackUserAdapter implements ChannelAdapter {
}

async start(): Promise<void> {
// Bolt requires the BOT token for Socket Mode event delivery.
// User tokens don't receive Socket Mode events reliably.
// The user token is kept separately for API calls (posting as user).
const boltToken = this.botTokenStr ?? this.userToken;
this.app = new App({ token: boltToken, appToken: this.appToken, socketMode: true });
this.userClient = new WebClient(this.userToken);

// Resolve own user ID (from user token)
Expand All @@ -101,18 +96,33 @@ export class SlackUserAdapter implements ChannelAdapter {
throw new Error(`Could not resolve user ID from token for team ${this.teamId}`);
}

// Resolve bot user ID (for echo loop prevention)
if (this.botClient) {
// Validate the bot token BEFORE handing it to Bolt. Constructing
// `new App({ token: <bad-bot-token>, ... })` triggers an internal
// auth.test that fires unhandled rejections (e.g. account_inactive)
// even if we later catch the bot client's own auth check. By
// validating up front we can drop a bad bot token and fall back to
// the user token for Bolt's Socket Mode connection.
let validatedBotToken: string | undefined;
if (this.botClient && this.botTokenStr) {
try {
const botAuth = await this.botClient.auth.test();
this.botUserId = (botAuth.user_id as string) ?? null;
validatedBotToken = this.botTokenStr;
log.info(`Bot identity loaded (${this.botUserId})`);
} catch {
log.warn(`Bot token auth failed -- agent will post as user`);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
log.warn(`Bot token auth failed (${message}) -- agent will post as user`);
this.botClient = null;
}
}

// Bolt requires the BOT token for Socket Mode event delivery when
// available; user tokens don't receive Socket Mode events reliably.
// If the bot token was rejected, fall back to the user token (Socket
// Mode events may be incomplete, but DMs still flow).
const boltToken = validatedBotToken ?? this.userToken;
this.app = new App({ token: boltToken, appToken: this.appToken, socketMode: true });

// Load all known user IDs for the owner across workspaces.
// This catches own messages even when Slack Connect surfaces them with
// a different workspace's user ID.
Expand Down
15 changes: 13 additions & 2 deletions src/ingest/sources/gmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ export class GmailIngestSource implements IngestSource {
private async *ingestAccount(
account: string | undefined,
options: IngestOptions,
cursor?: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_cursor?: string,
): AsyncGenerator<IngestMessage, void, undefined> {
const accessToken = await this.getAccessToken(account);

Expand All @@ -147,7 +148,17 @@ export class GmailIngestSource implements IngestSource {
query += ` to:${options.contact}`;
}

let pageToken: string | undefined = cursor || undefined;
// Important: do NOT pass the pipeline's cursor as Gmail's pageToken.
// The pipeline stores `lastMessage.id` (a Gmail message id, hex)
// as the cursor; pageToken is a different beast (an opaque,
// intra-run pagination handle, base64-ish). Cross-run reuse of a
// pageToken always 400s with `Invalid pageToken`.
//
// Re-running the same `q:` is safe — the pipeline's per-message
// dedup step rejects messages we've already stored, so a fresh full
// scan is idempotent. Delta semantics come from the `after:` clause
// (driven by `options.since`).
let pageToken: string | undefined;

while (true) {
const listUrl = new URL("https://gmail.googleapis.com/gmail/v1/users/me/messages");
Expand Down
Loading