Files
PaperClipAI/server/src/services/chat-slack-session-outbox.ts
T
DottaandPaperclip e733bddc45 perf(chat): stop polling provider action and session sync queues (#15822)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Chat stores pending provider actions in durable database rows.
> - Three action queues still scan every second when empty.
> - These scans keep idle databases active.
> - This pull request wakes those queues after a commit or at their next
deadline.
> - Empty queues stop querying while existing recovery rules remain in
place.

## Linked Issues or Issue Description

Refs #15808, #15802, #15625. Related sleep work: #15750, #15757.

**What existing behavior does this improve?**

Provider messages and notices, accepted GitHub webhook receipts, and
Slack session status synchronization.

**Current behavior**

Chat maintenance scans these three queues every second. It also rechecks
paused Slack sync rows every 30 seconds.

**Proposed behavior**

Reuse the transaction-aware action workers and existing chat_actions
rows. Wake after commits. Restore unfinished work on startup. Use stored
retry and stale-claim deadlines. Stop scanning when empty. Park paused
Slack sync rows until an endpoint change.

## What Changed

- Add three topics to the existing action worker registration. No
tables, migrations, triggers, or external services.
- Signal producers, claims, retry updates, terminal settlements, and
operator resolution inside their owning transaction. Provider I/O
remains outside transactions.
- Share pending-state and deadline predicates between dispatch and
scheduling. Exclude terminal history and uncertain provider sends.
- Preserve GitHub receipt attempt fences and provider-send quarantine.
Preserve Slack's 30-minute refresh while a session is processing.
- Remove these workers from periodic app maintenance. Update the sleep
inventory and add regression coverage.

## Verification

- Passed: 13 focused tests across scheduler, app wiring, and real
PostgreSQL integration. Covers actual commit wakes with deferred fast
paths disabled, GitHub retries and startup recovery, stale-send
quarantine, Slack publication-to-sync delivery, paused deadlines, and
idle queues.
- Passed: pnpm typecheck.
- Passed: full chat integration and related coordinator suites, **1,274
tests across five files**.
- Passed: pnpm build.
- Greptile: **5/5**, no actionable findings, completed on
`dc284cd275e5f6df624d60b27954421f2bcdbee2`.
- Hosted CI: **54 checks passed**, two intentionally skipped Storybook
checks, no failures or pending checks on
`dc284cd275e5f6df624d60b27954421f2bcdbee2`.

Provider responses are simulated. PostgreSQL, transactions, service
dispatch, and scheduling are real. No live Slack or GitHub App was used.

## Risks

- Writers must emit the existing process-local transaction signal.
Independent database clients and direct SQL still need explicit
integration or startup recovery.
- Accepted GitHub receipts are covered. Finding callbacks GitHub never
delivered still uses the separate provider recovery lane.
- Other chat maintenance and persistent transports still run. This does
not establish whole-instance sleep.
- Existing claim, revision, credential, and authorization checks remain
authoritative during concurrent foreground and scheduled dispatch.

## Model Used

OpenAI Codex (GPT-6), with repository inspection, code execution, and
automated tests.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-11 06:08:05 -05:00

75 lines
2.4 KiB
TypeScript

import { sql } from "drizzle-orm";
import { chatActions, type Db } from "@paperclipai/db";
import { notifyChatActionWork } from "./chat-work-notifications.js";
type DbOrTransaction = Db | Parameters<Parameters<Db["transaction"]>[0]>[0];
export interface SlackSessionSyncPayload {
version: 1;
revision: number;
runtimeGeneration: number;
credentialFingerprint: string;
}
export function slackSessionSyncPayload(
value: unknown,
): SlackSessionSyncPayload | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const payload = value as Partial<SlackSessionSyncPayload>;
return payload.version === 1 &&
Number.isSafeInteger(payload.revision) &&
payload.revision! > 0 &&
Number.isSafeInteger(payload.runtimeGeneration) &&
payload.runtimeGeneration! >= 0 &&
typeof payload.credentialFingerprint === "string" &&
payload.credentialFingerprint.length > 0
? (payload as SlackSessionSyncPayload)
: null;
}
/**
* One idempotent status lane per conversation. Publications and Stop receipts
* stage it in their own commit. It contains no provider content or credential
* material. Each new revision invalidates an old transport settlement.
*/
export async function stageSlackSessionSync(
database: DbOrTransaction,
input: {
companyId: string;
endpointId: string;
conversationId: string;
runtimeGeneration: number;
credentialFingerprint: string;
},
): Promise<void> {
const payload = {
version: 1,
revision: 1,
runtimeGeneration: input.runtimeGeneration,
credentialFingerprint: input.credentialFingerprint,
};
await notifyChatActionWork(database, "slack_session_sync");
await database
.insert(chatActions)
.values({
companyId: input.companyId,
endpointId: input.endpointId,
conversationId: input.conversationId,
providerActionId: `slack-session-sync:${input.conversationId}`,
kind: "slack_session_sync",
payload,
status: "received",
updatedAt: new Date(),
})
.onConflictDoUpdate({
target: [chatActions.endpointId, chatActions.providerActionId],
set: {
payload: sql`${JSON.stringify(payload)}::jsonb || jsonb_build_object('revision', coalesce((${chatActions.payload}->>'revision')::bigint, 0) + 1)`,
status: "received",
result: null,
updatedAt: new Date(),
},
});
}