mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 04:25:52 +01:00
## 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>
75 lines
2.4 KiB
TypeScript
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(),
|
|
},
|
|
});
|
|
}
|