Merge scoped health refreshes into company cleanup prerequisite

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/sleep-18-tool-connection-health:
  fix(server): scope health refreshes to changed connections
This commit is contained in:
DottaandPaperclip committed 2026-10-25 02:04:45 -05:00
commit e8febb30eb
25 files changed
+265 -91

No files matched your search

+1 -1
View File
@@ -324,7 +324,7 @@ export function accessService(db: Db) {
&& grant.credentialSecretRefs.length > 0
&& grant.credentialSecretRefs.every((ref) => !connectionSecretIdsToRemove.has(ref.secretId)),
);
if (!hasUnaffectedActiveGrant) await notifyToolConnectionHealth(tx, companyId);
if (!hasUnaffectedActiveGrant) await notifyToolConnectionHealth(tx, { companyId, id: connection.id });
await tx.update(toolConnections).set({
credentialRefs,
credentialSecretRefs,
@@ -2,7 +2,7 @@ import type { ChatProvider } from "@paperclipai/shared";
import { notifyToolConnectionHealth } from "../tool-connection-health-notifications.js";
import { chatConnectionTransport } from "./provider-catalog.js";
export async function notifyChatConnectionHealth(transaction: object, endpoint: { companyId: string; provider: ChatProvider }) {
export async function notifyChatConnectionHealth(transaction: object, endpoint: { companyId: string; connectionId: string; provider: ChatProvider }) {
if (chatConnectionTransport(endpoint.provider) === "chat_sdk") return;
await notifyToolConnectionHealth(transaction, endpoint.companyId);
await notifyToolConnectionHealth(transaction, { companyId: endpoint.companyId, id: endpoint.connectionId });
}
+5 -5
View File
@@ -914,7 +914,7 @@ export function emailChannelService(db: Db, options: EmailChannelOptions) {
updatedAt: now,
})
.where(eq(chatEndpoints.id, endpoint.id));
await notifyToolConnectionHealth(tx, endpoint.companyId);
await notifyToolConnectionHealth(tx, { companyId: endpoint.companyId, id: endpoint.connectionId });
await tx
.update(toolConnections)
.set({ status: "active", enabled: true, healthStatus: "ok" })
@@ -1986,7 +1986,7 @@ export function emailChannelService(db: Db, options: EmailChannelOptions) {
.update(chatEndpoints)
.set({ lastError: null, healthMessage: "Connected" })
.where(eq(chatEndpoints.id, endpoint.id));
await writeToolConnectionHealthChange(db, endpoint.companyId, tx => tx
await writeToolConnectionHealthChange(db, { companyId: endpoint.companyId, id: endpoint.connectionId }, tx => tx
.update(toolConnections)
.set({
healthStatus: "ok",
@@ -2094,7 +2094,7 @@ export function emailChannelService(db: Db, options: EmailChannelOptions) {
});
}
async function markError(endpoint: Endpoint, error: string) {
await writeToolConnectionHealthChange(db, endpoint.companyId, tx => tx
await writeToolConnectionHealthChange(db, { companyId: endpoint.companyId, id: endpoint.connectionId }, tx => tx
.update(toolConnections)
.set({
healthStatus: "degraded",
@@ -2364,7 +2364,7 @@ export function emailChannelService(db: Db, options: EmailChannelOptions) {
.update(chatEndpoints)
.set({ status: "paused" })
.where(eq(chatEndpoints.id, endpoint.id));
await notifyToolConnectionHealth(tx, endpoint.companyId);
await notifyToolConnectionHealth(tx, { companyId: endpoint.companyId, id: endpoint.connectionId });
await tx
.update(toolConnections)
.set({ enabled: false, status: "disabled" })
@@ -2458,7 +2458,7 @@ export function emailChannelService(db: Db, options: EmailChannelOptions) {
updatedAt: new Date(),
})
.where(eq(chatEndpoints.id, id));
await notifyToolConnectionHealth(tx, endpoint.companyId);
await notifyToolConnectionHealth(tx, { companyId: endpoint.companyId, id: endpoint.connectionId });
await tx
.update(toolConnections)
.set({
+1 -1
View File
@@ -258,7 +258,7 @@ export function emailConnectionService(
actorSource: actor.localImplicit ? "local_implicit" : "session",
},
);
await writeToolConnectionHealthChange(db, companyId, tx => tx
await writeToolConnectionHealthChange(db, connection, tx => tx
.update(toolConnections)
.set({
uid: `agentmail-account-${input.idempotencyKey}`,
@@ -327,7 +327,7 @@ export function githubConnectionEventService(
};
await database.update(connectionGrants).set({ providerTenant, updatedAt: now() })
.where(and(eq(connectionGrants.id, binding.grantId), eq(connectionGrants.companyId, binding.companyId)));
await notifyToolConnectionHealth(database, binding.companyId);
await notifyToolConnectionHealth(database, { companyId: binding.companyId, id: binding.connectionId });
await database.update(toolConnections).set({
healthStatus: unavailable ? "failed" : "ok",
healthMessage: unavailable
+5 -3
View File
@@ -691,7 +691,7 @@ export function smokeLabService(db: Db, options: {
));
const now = new Date();
if (existing) {
const [updated] = await writeToolConnectionHealthChange(db, input.companyId, tx => tx.update(toolApplications).set({
const [updated] = await writeToolConnectionHealthChange(db, { companyId: input.companyId, applicationId: existing.id }, tx => tx.update(toolApplications).set({
name: input.name,
description: input.description,
type: input.type,
@@ -743,7 +743,8 @@ export function smokeLabService(db: Db, options: {
lastHealthAt: now,
updatedAt: now,
};
return writeToolConnectionHealthChange(db, input.companyId, async tx => {
const connectionId = existing?.id ?? randomUUID();
return writeToolConnectionHealthChange(db, { companyId: input.companyId, id: connectionId }, async tx => {
const ensureDefaultOrganizationGrant = async (connectionId: string) => {
const [existingGrant] = await tx.select({ id: connectionGrants.id }).from(connectionGrants).where(and(
eq(connectionGrants.companyId, input.companyId),
@@ -769,6 +770,7 @@ export function smokeLabService(db: Db, options: {
return { row: updated ?? existing, created: false };
}
const [created] = await tx.insert(toolConnections).values({
id: connectionId,
companyId: input.companyId,
name: input.name,
uid: `smoke-lab/${input.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}`,
@@ -968,7 +970,7 @@ export function smokeLabService(db: Db, options: {
if (!connection || !httpSidecar) return;
const sidecarUrl = httpSidecar.url;
const now = new Date();
await writeToolConnectionHealthChange(db, companyId, tx => tx.update(toolConnections).set({
await writeToolConnectionHealthChange(db, connection, tx => tx.update(toolConnections).set({
config: {
...asRecord(connection.config),
url: `${sidecarUrl}/mcp`,
+11 -11
View File
@@ -1219,7 +1219,7 @@ export function toolAccessService(
safeDefault: true,
};
if (existing) {
const [updated] = await writeToolConnectionHealthChange(db, companyId, tx => tx
const [updated] = await writeToolConnectionHealthChange(db, { companyId, applicationId: existing.id }, tx => tx
.update(toolApplications)
.set({
name: definition.applicationName,
@@ -1262,7 +1262,7 @@ export function toolAccessService(
quarantineNewEntries: true,
};
if (existing) {
const updated = await writeToolConnectionHealthChange(db, companyId, async tx => {
const updated = await writeToolConnectionHealthChange(db, existing, async tx => {
const [updated] = await tx
.update(toolConnections)
.set({
@@ -1288,7 +1288,7 @@ export function toolAccessService(
return { row: updated, created: false };
}
const connectionId = randomUUID();
const created = await writeToolConnectionHealthChange(db, companyId, async tx => {
const created = await writeToolConnectionHealthChange(db, { companyId, id: connectionId }, async tx => {
const [created] = await tx
.insert(toolConnections)
.values({
@@ -2486,7 +2486,7 @@ export function toolAccessService(
personalIdentityUserId || dedicatedAgentId ? [] : credentialSecretRefs;
if (revivedConnectionPrevious) {
forgetMcpHttpSessions(revivedConnectionPrevious.id);
[connectionRow] = await writeToolConnectionHealthChange(db, companyId, tx => tx
[connectionRow] = await writeToolConnectionHealthChange(db, revivedConnectionPrevious, tx => tx
.update(toolConnections)
.set({
name,
@@ -2931,7 +2931,7 @@ export function toolAccessService(
connectionSetupMutationFingerprint(attemptedConnection),
);
if (connectionMutationIsStillCurrent) {
await notifyToolConnectionHealth(tx, companyId);
await notifyToolConnectionHealth(tx, revivedConnectionPrevious);
await tx
.update(toolConnections)
.set({
@@ -3007,7 +3007,7 @@ export function toolAccessService(
// surface the restoration failure instead of returning only the
// original provider error.
try {
await writeToolConnectionHealthChange(db, companyId, tx => tx
await writeToolConnectionHealthChange(db, revivedConnectionPrevious, tx => tx
.update(toolConnections)
.set({
status: "draft",
@@ -3033,7 +3033,7 @@ export function toolAccessService(
}
} else if (connectionRow) {
const failedConnectionId = connectionRow.id;
await writeToolConnectionHealthChange(db, companyId, tx => tx
await writeToolConnectionHealthChange(db, { companyId, id: failedConnectionId }, tx => tx
.delete(toolConnections)
.where(eq(toolConnections.id, failedConnectionId)))
.catch(() => undefined);
@@ -3148,7 +3148,7 @@ export function toolAccessService(
await tx.select({ id: toolApplications.id }).from(toolApplications)
.where(and(eq(toolApplications.id, connection.applicationId), eq(toolApplications.companyId, companyId)))
.for("update");
await notifyToolConnectionHealth(tx, companyId);
await notifyToolConnectionHealth(tx, connection);
const [row] = await tx.update(toolConnections).set({ config, transportConfig: config, status: "active", enabled: true, ...(input.agentInstructions !== undefined ? { agentInstructions: input.agentInstructions } : {}), updatedAt: new Date() })
.where(and(eq(toolConnections.id, connection.id), eq(toolConnections.companyId, companyId))).returning();
await tx.update(toolApplications).set({ status: "active", updatedAt: new Date() }).where(and(eq(toolApplications.id, connection.applicationId), eq(toolApplications.companyId, companyId)));
@@ -3505,7 +3505,7 @@ export function toolAccessService(
},
tx,
);
await notifyToolConnectionHealth(tx, companyId);
await notifyToolConnectionHealth(tx, connection);
const [updatedConnection] = await tx
.update(toolConnections)
.set({ status: "active", enabled: true, ...(input.agentInstructions !== undefined ? { agentInstructions: input.agentInstructions } : {}), updatedAt: new Date() })
@@ -4526,7 +4526,7 @@ export function toolAccessService(
}
const connectionId = randomUUID();
const binding = actorBinding(actor);
const row = await writeToolConnectionHealthChange(db, companyId, async tx => {
const row = await writeToolConnectionHealthChange(db, { companyId, id: connectionId }, async tx => {
const [row] = await tx
.insert(toolConnections)
.values({
@@ -4648,7 +4648,7 @@ export function toolAccessService(
await syncCredentialBindings(rows[0]!, [], tx);
return rows;
})
: await writeToolConnectionHealthChange(db, existing.companyId, async tx => {
: await writeToolConnectionHealthChange(db, existing, async tx => {
const rows = await write(tx);
await syncCredentialBindings(rows[0]!, [], tx);
await createToolRuntimeSlots(tx, runtimeSupervisor).ensureRuntimeSlot(rows[0]!);
@@ -1,38 +1,48 @@
import { logger } from "../../../middleware/logger.js";
import { and, eq, ne, sql } from "drizzle-orm";
import { and, asc, eq, gt, ne, sql } from "drizzle-orm";
import { toolApplications, toolConnections, type Db } from "@paperclipai/db";
import { isConnectorFeatureEnabled } from "@paperclipai/shared";
import { instanceSettingsService } from "../../instance-settings.js";
import { connectionSetupApp } from "../connections/setup-policy.js";
import type { ConnectionHealth } from "./health.js";
export type ConnectionHealthDeadline = { id: string; companyId: string; dueAt: number };
export type ConnectionHealthDeadline = { id: string; companyId: string; applicationId: string; dueAt: number };
const HEALTH_FRESHNESS_MS = 15 * 60_000;
export function connectionHealthSchedule(db: Db, checkHealth: ConnectionHealth["checkConnectionHealth"]) {
async function load(companyId?: string, connectionId?: string): Promise<ConnectionHealthDeadline[]> {
const rows = await db.select({
id: toolConnections.id,
companyId: toolConnections.companyId,
checkedAt: toolConnections.healthCheckedAt,
applicationKey: toolApplications.applicationKey,
sourceTemplateKey: sql<string | null>`coalesce(${toolConnections.config}->>'sourceTemplateKey', ${toolConnections.transportConfig}->>'sourceTemplateKey')`,
}).from(toolConnections).innerJoin(toolApplications, and(
eq(toolApplications.id, toolConnections.applicationId),
eq(toolApplications.companyId, toolConnections.companyId),
)).where(and(
eq(toolConnections.enabled, true), eq(toolConnections.status, "active"),
ne(toolConnections.transport, "chat_sdk"), ne(toolConnections.transport, "runtime_auth"),
ne(toolApplications.type, "paperclip_plugin"),
companyId === undefined ? undefined : eq(toolConnections.companyId, companyId),
connectionId === undefined ? undefined : eq(toolConnections.id, connectionId),
));
const definitions = rows.map(row => ({ row, app: connectionSetupApp(row.applicationKey, row.sourceTemplateKey) }));
const settings = definitions.some(({ app }) => app?.experimentalFeatureKey)
? await instanceSettingsService(db).getExperimental() : null;
return definitions.filter(({ app }) => !app || !settings || isConnectorFeatureEnabled(app, settings))
.map(({ row }) => ({ id: row.id, companyId: row.companyId,
dueAt: row.checkedAt === null ? 0 : row.checkedAt.getTime() + HEALTH_FRESHNESS_MS }));
async function load(companyId?: string, connectionId?: string, applicationId?: string, signal?: AbortSignal): Promise<ConnectionHealthDeadline[]> {
const deadlines: ConnectionHealthDeadline[] = [];
let cursor: string | undefined;
while (true) {
signal?.throwIfAborted();
const rows = await db.select({
id: toolConnections.id,
companyId: toolConnections.companyId,
applicationId: toolConnections.applicationId,
checkedAt: toolConnections.healthCheckedAt,
applicationKey: toolApplications.applicationKey,
sourceTemplateKey: sql<string | null>`coalesce(${toolConnections.config}->>'sourceTemplateKey', ${toolConnections.transportConfig}->>'sourceTemplateKey')`,
}).from(toolConnections).innerJoin(toolApplications, and(
eq(toolApplications.id, toolConnections.applicationId),
eq(toolApplications.companyId, toolConnections.companyId),
)).where(and(
eq(toolConnections.enabled, true), eq(toolConnections.status, "active"),
ne(toolConnections.transport, "chat_sdk"), ne(toolConnections.transport, "runtime_auth"),
ne(toolApplications.type, "paperclip_plugin"),
companyId === undefined ? undefined : eq(toolConnections.companyId, companyId),
connectionId === undefined ? undefined : eq(toolConnections.id, connectionId),
applicationId === undefined ? undefined : eq(toolConnections.applicationId, applicationId),
cursor === undefined ? undefined : gt(toolConnections.id, cursor),
)).orderBy(asc(toolConnections.id)).limit(100);
const definitions = rows.map(row => ({ row, app: connectionSetupApp(row.applicationKey, row.sourceTemplateKey) }));
const settings = definitions.some(({ app }) => app?.experimentalFeatureKey)
? await instanceSettingsService(db).getExperimental() : null;
deadlines.push(...definitions.filter(({ app }) => !app || !settings || isConnectorFeatureEnabled(app, settings))
.map(({ row }) => ({ id: row.id, companyId: row.companyId, applicationId: row.applicationId,
dueAt: row.checkedAt === null ? 0 : row.checkedAt.getTime() + HEALTH_FRESHNESS_MS })));
if (rows.length < 100) return deadlines;
cursor = rows[rows.length - 1].id;
}
}
async function check(id: string): Promise<ConnectionHealthDeadline | null> {
@@ -47,7 +47,7 @@ export function createConnectionHealth(db: Db, { now, audit, getConnectionRow, d
message: string | null,
) {
const now = new Date();
const [updated] = await writeToolConnectionHealthChange(db, connection.companyId, tx => tx
const [updated] = await writeToolConnectionHealthChange(db, connection, tx => tx
.update(toolConnections)
.set({
healthStatus: status,
@@ -269,7 +269,7 @@ export function createToolCatalogPublication(db: Db, { now, audit, getConnection
: refreshOptions.enableAllByDefault
? { ...connection.transportConfig, quarantineNewEntries: false }
: connection.transportConfig;
const [updatedConnection] = await writeToolConnectionHealthChange(db, connection.companyId, tx => tx
const [updatedConnection] = await writeToolConnectionHealthChange(db, connection, tx => tx
.update(toolConnections)
.set({
config: normalizedConfig,
@@ -537,7 +537,7 @@ async function removeConnection(
.returning({ id: connectionTokenIssuances.id });
const archived = await db.transaction(async (tx) => {
await notifyToolConnectionHealth(tx, connection.companyId);
await notifyToolConnectionHealth(tx, connection);
await tx.select({ id: toolApplications.id }).from(toolApplications)
.where(and(eq(toolApplications.id, connection.applicationId), eq(toolApplications.companyId, connection.companyId)))
.for("update");
@@ -113,7 +113,7 @@ async function finalizeOAuthAccess(
) {
throw conflict("This connection already uses a company identity");
}
connection = await writeToolConnectionHealthChange(db, companyId, async tx => {
connection = await writeToolConnectionHealthChange(db, connection, async tx => {
const [updated] = await tx
.update(toolConnections)
.set({
@@ -328,7 +328,7 @@ async function finalizeOAuthAccess(
updatedAt: nowAt,
})
.where(eq(connectionGrants.id, personalGrant.id));
await notifyToolConnectionHealth(tx, companyId);
await notifyToolConnectionHealth(tx, connection);
[connection] = await tx
.update(toolConnections)
.set({
@@ -356,7 +356,7 @@ export function createCloudOAuthCallback(db: Db, dependencies: Dependencies) {
scopes: [...profile.scopes],
},
};
await notifyToolConnectionHealth(tx, connection.companyId);
await notifyToolConnectionHealth(tx, connection);
[connection] = await tx
.update(toolConnections)
.set({
@@ -60,7 +60,7 @@ const oauthRefreshFlights = new Map<string, Promise<unknown>>();
reauthorizationRequiredAt: now().toISOString(),
},
};
const [updated] = await writeToolConnectionHealthChange(db, connection.companyId, tx => tx
const [updated] = await writeToolConnectionHealthChange(db, connection, tx => tx
.update(toolConnections)
.set({
status: "draft",
@@ -332,7 +332,7 @@ export function createDirectOAuthCallback(db: Db, dependencies: Dependencies) {
},
},
};
await notifyToolConnectionHealth(tx, connection.companyId);
await notifyToolConnectionHealth(tx, connection);
const [updatedConnection] = await tx
.update(toolConnections)
.set({
@@ -612,7 +612,7 @@ export function createDirectOAuthCallback(db: Db, dependencies: Dependencies) {
},
},
};
await notifyToolConnectionHealth(tx, connection.companyId);
await notifyToolConnectionHealth(tx, connection);
const [updatedConnection] = await tx
.update(toolConnections)
.set({
@@ -394,7 +394,7 @@ export function createOAuthGrantRefresh(db: Db, { now, store, leases, currentClo
connection.id,
connection.companyId,
);
const [reauthorizationRequired] = await writeToolConnectionHealthChange(db, latestConnection.companyId, tx => tx
const [reauthorizationRequired] = await writeToolConnectionHealthChange(db, latestConnection, tx => tx
.update(toolConnections)
.set({
status: "draft",
@@ -184,7 +184,7 @@ export function createVercelOAuthCallback(db: Db, dependencies: Dependencies) {
status: connection.status,
enabled: connection.enabled,
};
[connection] = await writeToolConnectionHealthChange(db, connection.companyId, tx => tx
[connection] = await writeToolConnectionHealthChange(db, connection, tx => tx
.update(toolConnections)
.set({
status: "active",
@@ -1,12 +1,27 @@
import type { Db } from "@paperclipai/db";
import { DELIVERY_QUEUES, notifyDeliveryWork } from "./delivery-work-notifications.js";
export const notifyToolConnectionHealth = (transaction: object, companyId: string) =>
notifyDeliveryWork(transaction, DELIVERY_QUEUES.toolConnectionHealth, companyId);
type HealthChange = string | { companyId: string; id: string } | { companyId: string; applicationId: string };
export type HealthScope = { companyId?: string; connectionId?: string; applicationId?: string };
export function writeToolConnectionHealthChange<T>(db: Db, companyId: string, write: (transaction: Db) => Promise<T>): Promise<T> {
export function healthScope(hint: string): HealthScope {
if (hint === "*") return {};
const [companyId, kind, id, extra] = hint.split(":");
if (companyId && kind === undefined) return { companyId };
if (companyId && id && extra === undefined) {
if (kind === "connection") return { companyId, connectionId: id };
if (kind === "application") return { companyId, applicationId: id };
}
throw new Error("Invalid tool connection health scope");
}
export const notifyToolConnectionHealth = (transaction: object, target: HealthChange) =>
notifyDeliveryWork(transaction, DELIVERY_QUEUES.toolConnectionHealth, typeof target === "string" ? target
: "id" in target ? `${target.companyId}:connection:${target.id}` : `${target.companyId}:application:${target.applicationId}`);
export function writeToolConnectionHealthChange<T>(db: Db, target: HealthChange, write: (transaction: Db) => Promise<T>): Promise<T> {
return db.transaction(async tx => {
await notifyToolConnectionHealth(tx, companyId);
await notifyToolConnectionHealth(tx, target);
return write(tx as unknown as Db);
});
}
@@ -1,3 +1,4 @@
import { healthScope, type HealthScope } from "./tool-connection-health-notifications.js";
import { takeCompanyWorkBatch } from "./company-work-batch.js";
import type { createDeliveryWorkCoordinator } from "./delivery-work-coordinator.js";
import { DELIVERY_QUEUES } from "./delivery-work-notifications.js";
@@ -6,7 +7,7 @@ import type { ConnectionHealthDeadline } from "./tool-access/catalog/health-sche
export function registerToolConnectionHealthWork(
coordinator: ReturnType<typeof createDeliveryWorkCoordinator>,
service: {
load(companyId?: string): Promise<ConnectionHealthDeadline[]>;
load(companyId?: string, connectionId?: string, applicationId?: string, signal?: AbortSignal): Promise<ConnectionHealthDeadline[]>;
check(id: string): Promise<ConnectionHealthDeadline | null>;
},
canRun: () => boolean,
@@ -14,6 +15,10 @@ export function registerToolConnectionHealthWork(
const dirty = new Set<string>(["*"]);
const deadlines = new Map<string, ConnectionHealthDeadline>();
const failures = new Map<string, { count: number; retryAt: number }>();
const matches = (entry: ConnectionHealthDeadline, scope: HealthScope) =>
(scope.companyId === undefined || entry.companyId === scope.companyId)
&& (scope.connectionId === undefined || entry.id === scope.connectionId)
&& (scope.applicationId === undefined || entry.applicationId === scope.applicationId);
const retryMs = 30_000;
let earliest = 0;
let lastCompany: string | null = null;
@@ -38,21 +43,26 @@ export function registerToolConnectionHealthWork(
const scopes = dirty.has("*") ? ["*"] : [...dirty];
dirty.clear();
const errors: unknown[] = [];
const unavailable = new Set<string>();
const unavailable: HealthScope[] = [];
for (let index = 0; index < scopes.length; index++) {
const scope = scopes[index];
if (signal.aborted || !canRun()) { for (const remaining of scopes.slice(index)) dirty.add(remaining); return; }
let target: HealthScope | undefined;
try {
const rows = await service.load(scope === "*" ? undefined : scope);
for (const entry of deadlines.values()) if (scope === "*" || entry.companyId === scope) deadlines.delete(entry.id);
target = healthScope(scope);
const rows = await service.load(target.companyId, target.connectionId, target.applicationId, signal);
if (target.connectionId) {
const current = deadlines.get(target.connectionId);
if (current && matches(current, target)) deadlines.delete(current.id);
} else for (const entry of deadlines.values()) if (matches(entry, target)) deadlines.delete(entry.id);
for (const entry of rows) deadlines.set(entry.id, entry);
for (const id of failures.keys()) {
const current = deadlines.get(id);
if (!current || current.dueAt > Date.now()) failures.delete(id);
}
} catch (error) { dirty.add(scope); unavailable.add(scope); errors.push(error); }
} catch (error) { dirty.add(target ? scope : "*"); unavailable.push(target ?? {}); errors.push(error); }
}
const due = [...deadlines.values()].filter(entry => !unavailable.has("*") && !unavailable.has(entry.companyId)
for (const id of failures.keys()) {
const current = deadlines.get(id);
if (!current || current.dueAt > Date.now()) failures.delete(id);
}
const due = [...deadlines.values()].filter(entry => !unavailable.some(scope => matches(entry, scope))
&& Math.max(entry.dueAt, failures.get(entry.id)?.retryAt ?? 0) <= Date.now())
.sort((a, b) => a.dueAt - b.dueAt || a.id.localeCompare(b.id));
const batch = takeCompanyWorkBatch(due, 25, lastCompany);
+1 -1
View File
@@ -3805,7 +3805,7 @@ export function createToolGatewayService(
message: string | null,
) {
const now = new Date();
await writeToolConnectionHealthChange(db, connection.companyId, tx => tx
await writeToolConnectionHealthChange(db, connection, tx => tx
.update(toolConnections)
.set({
healthStatus: status,