@agent-team-foundation/first-tree-hub 0.10.14 → 0.10.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -3,7 +3,7 @@ import { o as logFormatSchema, s as logLevelSchema } from "./logger-core-BTmvdfl
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
-
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { parse, stringify } from "yaml";
|
|
9
9
|
//#region ../shared/dist/config/index.mjs
|
|
@@ -650,6 +650,7 @@ const serverConfigSchema = defineConfig({
|
|
|
650
650
|
//#region src/core/bootstrap.ts
|
|
651
651
|
var bootstrap_exports = /* @__PURE__ */ __exportAll({
|
|
652
652
|
AuthRefreshFailedError: () => AuthRefreshFailedError,
|
|
653
|
+
AuthRefreshRateLimitedError: () => AuthRefreshRateLimitedError,
|
|
653
654
|
ensureFreshAccessToken: () => ensureFreshAccessToken,
|
|
654
655
|
ensureFreshAdminToken: () => ensureFreshAdminToken,
|
|
655
656
|
loadCredentials: () => loadCredentials,
|
|
@@ -706,6 +707,39 @@ var AuthRefreshFailedError = class extends Error {
|
|
|
706
707
|
}
|
|
707
708
|
};
|
|
708
709
|
/**
|
|
710
|
+
* Thrown when `/auth/refresh` returns 429. Carries the server-suggested
|
|
711
|
+
* retry-after (or a sane default) so the WS reconnect loop can wait at
|
|
712
|
+
* least that long instead of pounding the limiter inside the same window
|
|
713
|
+
* with its default 1/2/4/8s exponential backoff — which would just keep
|
|
714
|
+
* the rate-limit bucket full and stretch the outage. Defaults to 30s when
|
|
715
|
+
* the server omits the header.
|
|
716
|
+
*/
|
|
717
|
+
var AuthRefreshRateLimitedError = class extends Error {
|
|
718
|
+
retryAfterMs;
|
|
719
|
+
constructor(retryAfterMs, message) {
|
|
720
|
+
super(message ?? `Refresh request rate-limited; retry after ${Math.round(retryAfterMs / 1e3)}s.`);
|
|
721
|
+
this.name = "AuthRefreshRateLimitedError";
|
|
722
|
+
this.retryAfterMs = retryAfterMs;
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
/**
|
|
726
|
+
* Parse an HTTP `Retry-After` header. Accepts either an integer seconds
|
|
727
|
+
* value (the form fastify-rate-limit emits) or an RFC 7231 HTTP-date.
|
|
728
|
+
* Returns ms, or `null` when the header is absent / malformed.
|
|
729
|
+
*/
|
|
730
|
+
function parseRetryAfterMs(header) {
|
|
731
|
+
if (!header) return null;
|
|
732
|
+
const trimmed = header.trim();
|
|
733
|
+
const seconds = Number(trimmed);
|
|
734
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
735
|
+
const date = Date.parse(trimmed);
|
|
736
|
+
if (Number.isFinite(date)) {
|
|
737
|
+
const delta = date - Date.now();
|
|
738
|
+
return delta > 0 ? delta : 0;
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
709
743
|
* In-flight refresh promise. Multiple callers (WS handshake, proactive
|
|
710
744
|
* refresh timer, every SDK request) can see an expired token within the same
|
|
711
745
|
* millisecond — without dedupe each would fire an independent `/auth/refresh`
|
|
@@ -747,6 +781,7 @@ async function ensureFreshAccessToken(opts) {
|
|
|
747
781
|
signal: AbortSignal.timeout(1e4)
|
|
748
782
|
});
|
|
749
783
|
if (res.status === 401) throw new AuthRefreshFailedError("Refresh token rejected by server. Re-run `first-tree-hub connect <token>` (get a fresh token from the Web Computers page → New Connection).");
|
|
784
|
+
if (res.status === 429) throw new AuthRefreshRateLimitedError(parseRetryAfterMs(res.headers.get("retry-after")) ?? 3e4);
|
|
750
785
|
if (!res.ok) throw new Error(`Refresh request failed with status ${res.status}.`);
|
|
751
786
|
const data = await res.json();
|
|
752
787
|
saveCredentials({
|
|
@@ -775,13 +810,34 @@ function isTokenStale(token, minValidityMs) {
|
|
|
775
810
|
return true;
|
|
776
811
|
}
|
|
777
812
|
}
|
|
778
|
-
/**
|
|
813
|
+
/**
|
|
814
|
+
* Persist credentials to disk atomically.
|
|
815
|
+
*
|
|
816
|
+
* Plain `writeFileSync` opens with `O_TRUNC` then writes — between those
|
|
817
|
+
* calls the file is empty, and a concurrent `loadCredentials()` (e.g. a
|
|
818
|
+
* background daemon refreshing while the user runs a foreground CLI command)
|
|
819
|
+
* reads "" → `JSON.parse` throws → we fall back to "no credentials" and
|
|
820
|
+
* surface a misleading "run `client connect` again" error. write-to-temp +
|
|
821
|
+
* rename gives readers an all-or-nothing view: they see the old file or the
|
|
822
|
+
* new file, never a half-written one. Server-side the sliding-window design
|
|
823
|
+
* already accepts last-writer-wins semantics for the refresh token itself
|
|
824
|
+
* (see auth service comment), so atomicity at the file level is enough.
|
|
825
|
+
*/
|
|
779
826
|
function saveCredentials(creds) {
|
|
780
827
|
mkdirSync(dirname(CREDENTIALS_PATH), {
|
|
781
828
|
recursive: true,
|
|
782
829
|
mode: 448
|
|
783
830
|
});
|
|
784
|
-
|
|
831
|
+
const tmp = `${CREDENTIALS_PATH}.tmp.${process.pid}`;
|
|
832
|
+
try {
|
|
833
|
+
writeFileSync(tmp, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
834
|
+
renameSync(tmp, CREDENTIALS_PATH);
|
|
835
|
+
} catch (err) {
|
|
836
|
+
try {
|
|
837
|
+
unlinkSync(tmp);
|
|
838
|
+
} catch {}
|
|
839
|
+
throw err;
|
|
840
|
+
}
|
|
785
841
|
}
|
|
786
842
|
/**
|
|
787
843
|
* Load persisted credentials saved by the `connect` command.
|
|
@@ -808,4 +864,4 @@ function saveAgentConfig(agentName, agentId, runtime) {
|
|
|
808
864
|
return agentDir;
|
|
809
865
|
}
|
|
810
866
|
//#endregion
|
|
811
|
-
export {
|
|
867
|
+
export { resetConfigMeta as C, setConfigValue as E, resetConfig as S, serverConfigSchema as T, getConfigValue as _, ensureFreshAdminToken as a, migrateLegacyHome as b, resolveServerUrl as c, DEFAULT_CONFIG_DIR as d, DEFAULT_DATA_DIR as f, collectMissingPrompts as g, clientConfigSchema as h, ensureFreshAccessToken as i, saveAgentConfig as l, agentConfigSchema as m, AuthRefreshRateLimitedError as n, loadCredentials as o, DEFAULT_HOME_DIR as p, bootstrap_exports as r, resolveAccessToken as s, AuthRefreshFailedError as t, saveCredentials as u, initConfig as v, resolveConfigReadonly as w, readConfigFile as x, loadAgents as y };
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "../observability-DPyf745N-BSc8QNcR.mjs";
|
|
3
|
-
import { $ as findStaleAliases, A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, I as checkWebSocket, L as printResults, M as checkNodeVersion, N as checkServerConfig, O as checkBackgroundService, P as checkServerHealth, R as reconcileAgentConfigs, S as saveOnboardState, T as migrateLocalAgentDirs, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, _t as probeCapabilities, a as declineUpdate, at as fail, b as onboardCheck, c as detectInstallMode, ct as print, d as startServer, dt as ClientOrgMismatchError, et as formatStaleReason, f as COMMAND_VERSION, ft as ClientUserMismatchError, g as promptAddAgent, gt as cleanWorkspaces, h as isInteractive, ht as SessionRegistry, i as createExecuteUpdate, it as resolveReplyToFromEnv, j as checkDocker, k as checkClientConfig, l as fetchLatestVersion, lt as setJsonMode, m as uploadClientCapabilities, mt as SdkError, nt as createOwner, o as promptUpdate, ot as success, p as reconcileLocalRuntimeProviders, pt as FirstTreeHubSDK, r as registerSaaSConnectCommand, s as PACKAGE_NAME, tt as removeLocalAgent, u as installGlobalLatest, v as formatCheckReport, vt as applyClientLoggerConfig, w as createApiNameResolver, x as onboardCreate, y as loadOnboardState, yt as configureClientLoggerForService, z as getClientServiceStatus } from "../saas-connect-
|
|
3
|
+
import { $ as findStaleAliases, A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, I as checkWebSocket, L as printResults, M as checkNodeVersion, N as checkServerConfig, O as checkBackgroundService, P as checkServerHealth, R as reconcileAgentConfigs, S as saveOnboardState, T as migrateLocalAgentDirs, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, _t as probeCapabilities, a as declineUpdate, at as fail, b as onboardCheck, c as detectInstallMode, ct as print, d as startServer, dt as ClientOrgMismatchError, et as formatStaleReason, f as COMMAND_VERSION, ft as ClientUserMismatchError, g as promptAddAgent, gt as cleanWorkspaces, h as isInteractive, ht as SessionRegistry, i as createExecuteUpdate, it as resolveReplyToFromEnv, j as checkDocker, k as checkClientConfig, l as fetchLatestVersion, lt as setJsonMode, m as uploadClientCapabilities, mt as SdkError, nt as createOwner, o as promptUpdate, ot as success, p as reconcileLocalRuntimeProviders, pt as FirstTreeHubSDK, r as registerSaaSConnectCommand, s as PACKAGE_NAME, tt as removeLocalAgent, u as installGlobalLatest, v as formatCheckReport, vt as applyClientLoggerConfig, w as createApiNameResolver, x as onboardCreate, y as loadOnboardState, yt as configureClientLoggerForService, z as getClientServiceStatus } from "../saas-connect-CebXWFF-.mjs";
|
|
4
4
|
import "../logger-core-BTmvdflj-DjW8FM4T.mjs";
|
|
5
|
-
import { C as
|
|
5
|
+
import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, _ as getConfigValue, a as ensureFreshAdminToken, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, x as readConfigFile, y as loadAgents } from "../bootstrap-DUeYbwm-.mjs";
|
|
6
6
|
import "../dist-D6AOiyNg.mjs";
|
|
7
7
|
import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-DQ1l18Ah.mjs";
|
|
8
8
|
import "../invitation-B1pjAyOz-BaCA9PII.mjs";
|
|
@@ -1608,13 +1608,13 @@ function isSecretField(schema, dotPath) {
|
|
|
1608
1608
|
//#region src/commands/onboard.ts
|
|
1609
1609
|
async function promptMissing(args) {
|
|
1610
1610
|
if (!args.server) try {
|
|
1611
|
-
const { resolveServerUrl } = await import("../bootstrap-
|
|
1611
|
+
const { resolveServerUrl } = await import("../bootstrap-DUeYbwm-.mjs").then((n) => n.r);
|
|
1612
1612
|
resolveServerUrl();
|
|
1613
1613
|
} catch {
|
|
1614
1614
|
args.server = await input({ message: "Hub server URL:" });
|
|
1615
1615
|
saveOnboardState(args);
|
|
1616
1616
|
}
|
|
1617
|
-
const { loadCredentials } = await import("../bootstrap-
|
|
1617
|
+
const { loadCredentials } = await import("../bootstrap-DUeYbwm-.mjs").then((n) => n.r);
|
|
1618
1618
|
if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
|
|
1619
1619
|
if (!args.id) {
|
|
1620
1620
|
args.id = await input({ message: "Agent ID:" });
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "./observability-DPyf745N-BSc8QNcR.mjs";
|
|
2
|
-
import { A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, H as resolveCliInvocation, I as checkWebSocket, J as isDockerAvailable, K as uninstallClientService, L as printResults, M as checkNodeVersion, N as checkServerConfig, P as checkServerHealth, Q as rotateClientIdWithBackup, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, b as onboardCheck, d as startServer, g as promptAddAgent, h as isInteractive, j as checkDocker, k as checkClientConfig, mt as SdkError, n as deriveHubUrlFromToken, nt as createOwner, pt as FirstTreeHubSDK, q as ensurePostgres, rt as hasUser, st as blank, t as HubUrlDerivationError, ut as status, v as formatCheckReport, x as onboardCreate, z as getClientServiceStatus } from "./saas-connect-
|
|
2
|
+
import { A as checkDatabase, B as installClientService, C as runHomeMigration, D as checkAgentConfigs, E as runMigrations, F as checkServerReachable, G as stopClientService, H as resolveCliInvocation, I as checkWebSocket, J as isDockerAvailable, K as uninstallClientService, L as printResults, M as checkNodeVersion, N as checkServerConfig, P as checkServerHealth, Q as rotateClientIdWithBackup, U as restartClientService, V as isServiceSupported, W as startClientService, X as ClientRuntime, Y as stopPostgres, Z as handleClientOrgMismatch, _ as promptMissingFields, b as onboardCheck, d as startServer, g as promptAddAgent, h as isInteractive, j as checkDocker, k as checkClientConfig, mt as SdkError, n as deriveHubUrlFromToken, nt as createOwner, pt as FirstTreeHubSDK, q as ensurePostgres, rt as hasUser, st as blank, t as HubUrlDerivationError, ut as status, v as formatCheckReport, x as onboardCreate, z as getClientServiceStatus } from "./saas-connect-CebXWFF-.mjs";
|
|
3
3
|
import "./logger-core-BTmvdflj-DjW8FM4T.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { a as ensureFreshAdminToken, c as resolveServerUrl, i as ensureFreshAccessToken, n as AuthRefreshRateLimitedError, s as resolveAccessToken, t as AuthRefreshFailedError } from "./bootstrap-DUeYbwm-.mjs";
|
|
5
5
|
import "./dist-D6AOiyNg.mjs";
|
|
6
6
|
import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-DQ1l18Ah.mjs";
|
|
7
7
|
import "./invitation-B1pjAyOz-BaCA9PII.mjs";
|
|
8
|
-
export { AuthRefreshFailedError, ClientRuntime, FirstTreeHubSDK, HubUrlDerivationError, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, deriveHubUrlFromToken, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, restartClientService, rotateClientIdWithBackup, runHomeMigration, runMigrations, startClientService, startServer, status, stopClientService, stopPostgres, uninstallClientService };
|
|
8
|
+
export { AuthRefreshFailedError, AuthRefreshRateLimitedError, ClientRuntime, FirstTreeHubSDK, HubUrlDerivationError, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, deriveHubUrlFromToken, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, restartClientService, rotateClientIdWithBackup, runHomeMigration, runMigrations, startClientService, startServer, status, stopClientService, stopPostgres, uninstallClientService };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { m as __toESM } from "./esm-CYu4tXXn.mjs";
|
|
2
2
|
import { _ as withSpan, a as endWsConnectionSpan, b as require_pino, c as messageAttrs, d as rootLogger$1, g as startWsConnectionSpan, i as currentTraceId, n as applyLoggerConfig, o as getFastifyOtelPlugin, p as setWsConnectionAttrs, r as createLogger$1, t as adapterAttrs, u as observabilityPlugin, v as withWsMessageSpan, y as FIRST_TREE_HUB_ATTR } from "./observability-DPyf745N-BSc8QNcR.mjs";
|
|
3
|
-
import { C as
|
|
3
|
+
import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, b as migrateLegacyHome, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR$1, g as collectMissingPrompts, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR$1, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, y as loadAgents } from "./bootstrap-DUeYbwm-.mjs";
|
|
4
4
|
import { $ as refreshTokenSchema, A as createOrgFromMeSchema, B as imageInlineContentSchema, C as clientRegisterSchema, D as createAgentSchema, E as createAdapterMappingSchema, F as dryRunAgentRuntimeConfigSchema, G as isReservedAgentName$1, H as inboxDeliverFrameSchema$1, I as extractMentions, J as loginSchema, K as joinByInvitationSchema, L as githubCallbackQuerySchema, M as createTaskSchema, N as defaultRuntimeConfigPayload, O as createChatSchema, P as delegateFeishuUserSchema, Q as rebindAgentSchema, R as githubDevCallbackQuerySchema, S as clientCapabilitiesSchema$1, St as wsAuthFrameSchema, T as createAdapterConfigSchema, U as inboxPollQuerySchema, V as inboxAckFrameSchema, W as isRedactedEnvValue, X as notificationQuerySchema, Y as messageSourceSchema$1, Z as paginationQuerySchema, _ as adminUpdateTaskSchema, _t as updateClientCapabilitiesSchema, a as AGENT_STATUSES, at as sendToAgentSchema, b as agentRuntimeConfigPayloadSchema$1, bt as updateSystemConfigSchema, ct as sessionEventSchema$1, d as TASK_HEALTH_SIGNALS, dt as switchOrgSchema, et as runtimeStateMessageSchema, f as TASK_STATUSES, ft as taskListQuerySchema, g as adminCreateTaskSchema, gt as updateChatSchema, h as addParticipantSchema, ht as updateAgentSchema, i as AGENT_SOURCES, it as sendMessageSchema, j as createOrganizationSchema, k as createMemberSchema, l as SYSTEM_CONFIG_DEFAULTS, lt as sessionReconcileRequestSchema, m as WS_AUTH_FRAME_TIMEOUT_MS, mt as updateAgentRuntimeConfigSchema, n as AGENT_NAME_REGEX$1, nt as scanMentionTokens, o as AGENT_TYPES, ot as sessionCompletionMessageSchema, p as TASK_TERMINAL_STATUSES, pt as updateAdapterConfigSchema, q as linkTaskChatSchema, r as AGENT_SELECTOR_HEADER$1, rt as selfServiceFeishuBotSchema, s as AGENT_VISIBILITY, st as sessionEventMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as safeRedirectPath, u as TASK_CREATOR_TYPES, ut as sessionStateMessageSchema, v as agentBindRequestSchema, vt as updateMemberSchema, w as connectTokenExchangeSchema, x as agentTypeSchema$1, xt as updateTaskStatusSchema, y as agentPinnedMessageSchema$1, yt as updateOrganizationSchema, z as githubStartQuerySchema } from "./dist-D6AOiyNg.mjs";
|
|
5
5
|
import { _ as recordRedemption, a as ConflictError, b as uuidv7, c as UnauthorizedError, d as findActiveByToken, f as getActiveInvitation, h as organizations, i as ClientUserMismatchError$1, l as buildInviteUrl, m as invitations, n as BadRequestError, o as ForbiddenError, p as invitationRedemptions, r as ClientOrgMismatchError$1, s as NotFoundError, t as AppError, u as ensureActiveInvitation, y as users } from "./invitation-B1pjAyOz-BaCA9PII.mjs";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
@@ -21,7 +21,7 @@ import { fileURLToPath } from "node:url";
|
|
|
21
21
|
import * as semver from "semver";
|
|
22
22
|
import { confirm, input, password, select } from "@inquirer/prompts";
|
|
23
23
|
import bcrypt from "bcrypt";
|
|
24
|
-
import { and, asc, count, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, or, sql } from "drizzle-orm";
|
|
24
|
+
import { and, asc, count, desc, eq, gt, gte, inArray, isNotNull, isNull, lt, ne, or, sql } from "drizzle-orm";
|
|
25
25
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
26
26
|
import postgres from "postgres";
|
|
27
27
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
@@ -1883,6 +1883,13 @@ var ClientConnection = class extends EventEmitter {
|
|
|
1883
1883
|
/** Fires ~60s before JWT exp so we reconnect with a fresh token first. */
|
|
1884
1884
|
authRefreshTimer = null;
|
|
1885
1885
|
reconnectAttempt = 0;
|
|
1886
|
+
/**
|
|
1887
|
+
* If the most recent refresh attempt was rate-limited (HTTP 429), the
|
|
1888
|
+
* server-suggested wait in ms — consumed by the next `scheduleReconnect`
|
|
1889
|
+
* to floor its delay so we don't keep retrying inside the same 60s
|
|
1890
|
+
* limiter window. Cleared after one use.
|
|
1891
|
+
*/
|
|
1892
|
+
nextReconnectMinDelayMs = 0;
|
|
1886
1893
|
closing = false;
|
|
1887
1894
|
registered = false;
|
|
1888
1895
|
/** Count of `server:welcome` frames received; drives `isReconnect` flag. */
|
|
@@ -2095,6 +2102,10 @@ var ClientConnection = class extends EventEmitter {
|
|
|
2095
2102
|
if (e.name === "AuthRefreshFailedError") {
|
|
2096
2103
|
this.closing = true;
|
|
2097
2104
|
this.emit("auth:fatal", e);
|
|
2105
|
+
} else if (e.name === "AuthRefreshRateLimitedError") {
|
|
2106
|
+
const retryAfterMs = e.retryAfterMs ?? 3e4;
|
|
2107
|
+
this.nextReconnectMinDelayMs = Math.max(this.nextReconnectMinDelayMs, retryAfterMs);
|
|
2108
|
+
this.authLogger.warn({ retryAfterMs }, "refresh rate-limited; deferring reconnect");
|
|
2098
2109
|
}
|
|
2099
2110
|
settle(reject, e);
|
|
2100
2111
|
ws.close();
|
|
@@ -2348,10 +2359,18 @@ var ClientConnection = class extends EventEmitter {
|
|
|
2348
2359
|
if (this.closing) return;
|
|
2349
2360
|
this.reconnectAttempt++;
|
|
2350
2361
|
this.emit("reconnecting", this.reconnectAttempt);
|
|
2351
|
-
const
|
|
2362
|
+
const exponential = Math.min(RECONNECT_BASE_MS * 2 ** (this.reconnectAttempt - 1), RECONNECT_MAX_MS);
|
|
2363
|
+
const floor = this.nextReconnectMinDelayMs;
|
|
2364
|
+
this.nextReconnectMinDelayMs = 0;
|
|
2365
|
+
let delay = Math.max(exponential, floor);
|
|
2366
|
+
if (floor > 0) {
|
|
2367
|
+
const jitter = delay * .2 * (Math.random() * 2 - 1);
|
|
2368
|
+
delay = Math.max(0, Math.round(delay + jitter));
|
|
2369
|
+
}
|
|
2352
2370
|
this.wsLogger.debug({
|
|
2353
2371
|
attempt: this.reconnectAttempt,
|
|
2354
|
-
delayMs: delay
|
|
2372
|
+
delayMs: delay,
|
|
2373
|
+
floorMs: floor || void 0
|
|
2355
2374
|
}, "scheduling reconnect");
|
|
2356
2375
|
this.reconnectTimer = setTimeout(() => {
|
|
2357
2376
|
this.reconnectTimer = null;
|
|
@@ -2401,6 +2420,18 @@ var ClientConnection = class extends EventEmitter {
|
|
|
2401
2420
|
* before the server's scheduleAuthExpiry timer fires. Short-lived tokens
|
|
2402
2421
|
* (exp <= lead window) skip the proactive reconnect entirely — we let the
|
|
2403
2422
|
* server push `auth:expired` and handle that path.
|
|
2423
|
+
*
|
|
2424
|
+
* Order is "refresh-then-close", not "close-then-let-reconnect-refresh".
|
|
2425
|
+
* The earlier shape relied on the new connection's open handler to do the
|
|
2426
|
+
* `/auth/refresh` HTTP, which forced ≥1s of WS downtime per cycle even on
|
|
2427
|
+
* the happy path (one base reconnect delay + the refresh round-trip) and
|
|
2428
|
+
* compounded badly under 429: every retry attempt also closed/reopened the
|
|
2429
|
+
* WS, holding the agent offline for 15-20s while the limiter cooled down.
|
|
2430
|
+
* Refreshing first lets us swap the new token onto a still-open WS with no
|
|
2431
|
+
* observable disconnect when the refresh succeeds; the original close-and-
|
|
2432
|
+
* reconnect flow only runs on failure as a last-ditch fallback (it'll hit
|
|
2433
|
+
* the same 429 on its next retry, but at least the Retry-After floor is
|
|
2434
|
+
* now wired up so we don't pile attempts inside the same window).
|
|
2404
2435
|
*/
|
|
2405
2436
|
scheduleProactiveAuthRefresh(token) {
|
|
2406
2437
|
this.clearAuthRefreshTimer();
|
|
@@ -2410,12 +2441,29 @@ var ClientConnection = class extends EventEmitter {
|
|
|
2410
2441
|
if (delay <= 0) return;
|
|
2411
2442
|
this.authLogger.debug({ delayMs: delay }, "scheduled proactive auth refresh");
|
|
2412
2443
|
this.authRefreshTimer = setTimeout(() => {
|
|
2413
|
-
this.
|
|
2414
|
-
if (this.closing) return;
|
|
2415
|
-
this.authLogger.info("triggering proactive auth refresh");
|
|
2416
|
-
this.ws?.close(1e3, "proactive auth refresh");
|
|
2444
|
+
this.runProactiveAuthRefresh();
|
|
2417
2445
|
}, delay);
|
|
2418
2446
|
}
|
|
2447
|
+
async runProactiveAuthRefresh() {
|
|
2448
|
+
this.authRefreshTimer = null;
|
|
2449
|
+
if (this.closing) return;
|
|
2450
|
+
this.authLogger.info("triggering proactive auth refresh");
|
|
2451
|
+
try {
|
|
2452
|
+
await this.getAccessToken({ minValidityMs: AUTH_REFRESH_LEAD_MS + 5e3 });
|
|
2453
|
+
} catch (err) {
|
|
2454
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2455
|
+
if (e.name === "AuthRefreshRateLimitedError") {
|
|
2456
|
+
const retryAfterMs = e.retryAfterMs ?? 3e4;
|
|
2457
|
+
this.nextReconnectMinDelayMs = Math.max(this.nextReconnectMinDelayMs, retryAfterMs);
|
|
2458
|
+
this.authLogger.warn({ retryAfterMs }, "proactive refresh rate-limited; deferring reconnect");
|
|
2459
|
+
} else if (e.name === "AuthRefreshFailedError") {
|
|
2460
|
+
this.closing = true;
|
|
2461
|
+
this.emit("auth:fatal", e);
|
|
2462
|
+
return;
|
|
2463
|
+
} else this.authLogger.warn({ err: e }, "proactive refresh failed; falling back to reconnect path");
|
|
2464
|
+
}
|
|
2465
|
+
this.ws?.close(1e3, "proactive auth refresh");
|
|
2466
|
+
}
|
|
2419
2467
|
};
|
|
2420
2468
|
/** Built-in handler registry. Populated by handler modules. */
|
|
2421
2469
|
const HANDLER_REGISTRY = /* @__PURE__ */ new Map();
|
|
@@ -9078,7 +9126,7 @@ function createFeedbackHandler(config) {
|
|
|
9078
9126
|
return { handle };
|
|
9079
9127
|
}
|
|
9080
9128
|
//#endregion
|
|
9081
|
-
//#region ../server/dist/app-
|
|
9129
|
+
//#region ../server/dist/app-DFDhctwC.mjs
|
|
9082
9130
|
var __defProp = Object.defineProperty;
|
|
9083
9131
|
var __exportAll = (all, no_symbols) => {
|
|
9084
9132
|
let target = {};
|
|
@@ -11316,6 +11364,22 @@ function removeClientConnection(clientId, ws) {
|
|
|
11316
11364
|
clientConnections.delete(clientId);
|
|
11317
11365
|
return agentIds;
|
|
11318
11366
|
}
|
|
11367
|
+
/**
|
|
11368
|
+
* Was `ws` the socket currently registered as `clientId`'s active connection
|
|
11369
|
+
* at the time of the call? Used by ws-client.ts's `socket.on("close")` to
|
|
11370
|
+
* decide whether to write `clients.status='disconnected'` to the DB — when a
|
|
11371
|
+
* fast reconnect happens, the new socket has already swapped itself in via
|
|
11372
|
+
* `setClientConnection`, so the old socket's late-arriving onClose must NOT
|
|
11373
|
+
* stamp the row back to disconnected.
|
|
11374
|
+
*
|
|
11375
|
+
* The check is "this socket equals the registered ws", not "this socket is
|
|
11376
|
+
* still OPEN" — the close handler runs precisely when the socket is no
|
|
11377
|
+
* longer OPEN, but the in-memory entry might still legitimately point at
|
|
11378
|
+
* us if no new connection has taken over yet.
|
|
11379
|
+
*/
|
|
11380
|
+
function isActiveClientConnection(clientId, ws) {
|
|
11381
|
+
return clientConnections.get(clientId)?.ws === ws;
|
|
11382
|
+
}
|
|
11319
11383
|
/** Send a message to a client's WebSocket. Returns true if delivered. */
|
|
11320
11384
|
function sendToClient(clientId, message) {
|
|
11321
11385
|
const entry = clientConnections.get(clientId);
|
|
@@ -14406,18 +14470,11 @@ async function pollInbox(db, inboxId, limit) {
|
|
|
14406
14470
|
}
|
|
14407
14471
|
async function pollInboxInner(db, inboxId, limit) {
|
|
14408
14472
|
return db.transaction(async (tx) => {
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
14414
|
-
WHERE inbox_id = ${inboxId} AND status = 'pending' AND notify = true
|
|
14415
|
-
ORDER BY created_at
|
|
14416
|
-
LIMIT ${limit}
|
|
14417
|
-
FOR UPDATE SKIP LOCKED
|
|
14418
|
-
)
|
|
14419
|
-
RETURNING *
|
|
14420
|
-
`));
|
|
14473
|
+
const targetIds = tx.select({ id: inboxEntries.id }).from(inboxEntries).where(and(eq(inboxEntries.inboxId, inboxId), eq(inboxEntries.status, "pending"), eq(inboxEntries.notify, true))).orderBy(asc(inboxEntries.createdAt)).limit(limit).for("update", { skipLocked: true });
|
|
14474
|
+
return bundleDeliveryWithSilentContext(tx, inboxId, await tx.update(inboxEntries).set({
|
|
14475
|
+
status: "delivered",
|
|
14476
|
+
deliveredAt: /* @__PURE__ */ new Date()
|
|
14477
|
+
}).where(inArray(inboxEntries.id, targetIds)).returning());
|
|
14421
14478
|
});
|
|
14422
14479
|
}
|
|
14423
14480
|
/**
|
|
@@ -14430,7 +14487,7 @@ async function pollInboxInner(db, inboxId, limit) {
|
|
|
14430
14487
|
* hub-inbox-ws-data-plane §3.2 risk #1).
|
|
14431
14488
|
*
|
|
14432
14489
|
* Steps:
|
|
14433
|
-
* 1. Sort by `
|
|
14490
|
+
* 1. Sort by `createdAt` ASC (PG `RETURNING` does not guarantee order).
|
|
14434
14491
|
* 2. For each trigger, collect silent context & bulk-ack stale silent rows.
|
|
14435
14492
|
* 3. Fetch the trigger messages.
|
|
14436
14493
|
* 4. Build wire payloads via the single dispatcher.
|
|
@@ -14439,16 +14496,16 @@ async function pollInboxInner(db, inboxId, limit) {
|
|
|
14439
14496
|
*/
|
|
14440
14497
|
async function bundleDeliveryWithSilentContext(tx, inboxId, claimed) {
|
|
14441
14498
|
if (claimed.length === 0) return [];
|
|
14442
|
-
claimed.sort((a, b) => a.
|
|
14499
|
+
claimed.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
14443
14500
|
const precedingByEntryId = await collectPrecedingContext(tx, inboxId, claimed);
|
|
14444
|
-
const messageIds = claimed.map((e) => e.
|
|
14501
|
+
const messageIds = claimed.map((e) => e.messageId);
|
|
14445
14502
|
const msgs = await tx.select().from(messages).where(inArray(messages.id, messageIds));
|
|
14446
14503
|
const msgMap = new Map(msgs.map((m) => [m.id, m]));
|
|
14447
14504
|
const payloads = await buildClientMessagePayloadsForInbox(tx, inboxId, claimed.map((entry) => {
|
|
14448
|
-
const msg = msgMap.get(entry.
|
|
14449
|
-
if (!msg) throw new Error(`Unexpected: message ${entry.
|
|
14505
|
+
const msg = msgMap.get(entry.messageId);
|
|
14506
|
+
if (!msg) throw new Error(`Unexpected: message ${entry.messageId} not found`);
|
|
14450
14507
|
return {
|
|
14451
|
-
entryChatId: entry.
|
|
14508
|
+
entryChatId: entry.chatId,
|
|
14452
14509
|
precedingMessages: precedingByEntryId.get(entry.id) ?? [],
|
|
14453
14510
|
message: {
|
|
14454
14511
|
id: msg.id,
|
|
@@ -14469,15 +14526,15 @@ async function bundleDeliveryWithSilentContext(tx, inboxId, claimed) {
|
|
|
14469
14526
|
const payload = payloads[idx];
|
|
14470
14527
|
if (!payload) throw new Error(`Unexpected: payload for entry ${entry.id} not built`);
|
|
14471
14528
|
return {
|
|
14472
|
-
id:
|
|
14473
|
-
inboxId: entry.
|
|
14474
|
-
messageId: entry.
|
|
14475
|
-
chatId: entry.
|
|
14529
|
+
id: entry.id,
|
|
14530
|
+
inboxId: entry.inboxId,
|
|
14531
|
+
messageId: entry.messageId,
|
|
14532
|
+
chatId: entry.chatId,
|
|
14476
14533
|
status: entry.status,
|
|
14477
|
-
retryCount: entry.
|
|
14478
|
-
createdAt: entry.
|
|
14479
|
-
deliveredAt: entry.
|
|
14480
|
-
ackedAt: entry.
|
|
14534
|
+
retryCount: entry.retryCount,
|
|
14535
|
+
createdAt: entry.createdAt.toISOString(),
|
|
14536
|
+
deliveredAt: entry.deliveredAt?.toISOString() ?? null,
|
|
14537
|
+
ackedAt: entry.ackedAt?.toISOString() ?? null,
|
|
14481
14538
|
message: payload
|
|
14482
14539
|
};
|
|
14483
14540
|
});
|
|
@@ -14512,21 +14569,11 @@ async function claimAndBuildForPush(db, inboxId, messageId) {
|
|
|
14512
14569
|
"inbox.id": inboxId,
|
|
14513
14570
|
"message.id": messageId
|
|
14514
14571
|
}, () => db.transaction(async (tx) => {
|
|
14515
|
-
|
|
14516
|
-
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
WHERE inbox_id = ${inboxId}
|
|
14521
|
-
AND message_id = ${messageId}
|
|
14522
|
-
AND status = 'pending'
|
|
14523
|
-
AND notify = true
|
|
14524
|
-
ORDER BY created_at
|
|
14525
|
-
LIMIT ${PUSH_CLAIM_BATCH_LIMIT}
|
|
14526
|
-
FOR UPDATE SKIP LOCKED
|
|
14527
|
-
)
|
|
14528
|
-
RETURNING *
|
|
14529
|
-
`));
|
|
14572
|
+
const targetIds = tx.select({ id: inboxEntries.id }).from(inboxEntries).where(and(eq(inboxEntries.inboxId, inboxId), eq(inboxEntries.messageId, messageId), eq(inboxEntries.status, "pending"), eq(inboxEntries.notify, true))).orderBy(asc(inboxEntries.createdAt)).limit(PUSH_CLAIM_BATCH_LIMIT).for("update", { skipLocked: true });
|
|
14573
|
+
return bundleDeliveryWithSilentContext(tx, inboxId, await tx.update(inboxEntries).set({
|
|
14574
|
+
status: "delivered",
|
|
14575
|
+
deliveredAt: /* @__PURE__ */ new Date()
|
|
14576
|
+
}).where(inArray(inboxEntries.id, targetIds)).returning());
|
|
14530
14577
|
}));
|
|
14531
14578
|
}
|
|
14532
14579
|
/**
|
|
@@ -14549,7 +14596,7 @@ async function claimBacklogForPush(db, inboxId, limit) {
|
|
|
14549
14596
|
* `PRECEDING_CONTEXT_WINDOW_SECONDS`. Returned messages are oldest-first.
|
|
14550
14597
|
*
|
|
14551
14598
|
* Side effect: bulk-ack ALL silent pending rows in each chat with
|
|
14552
|
-
*
|
|
14599
|
+
* createdAt < latest_trigger.createdAt — including ones that fell outside
|
|
14553
14600
|
* the window/cap. Otherwise stale silent rows would accumulate and re-load
|
|
14554
14601
|
* on every poll.
|
|
14555
14602
|
*/
|
|
@@ -14557,51 +14604,41 @@ async function collectPrecedingContext(tx, inboxId, triggers) {
|
|
|
14557
14604
|
const result = /* @__PURE__ */ new Map();
|
|
14558
14605
|
const byChat = /* @__PURE__ */ new Map();
|
|
14559
14606
|
for (const t of triggers) {
|
|
14560
|
-
if (t.
|
|
14561
|
-
const list = byChat.get(t.
|
|
14607
|
+
if (t.chatId === null) continue;
|
|
14608
|
+
const list = byChat.get(t.chatId) ?? [];
|
|
14562
14609
|
list.push(t);
|
|
14563
|
-
byChat.set(t.
|
|
14610
|
+
byChat.set(t.chatId, list);
|
|
14564
14611
|
}
|
|
14565
14612
|
for (const [chatId, chatTriggers] of byChat) {
|
|
14566
|
-
chatTriggers.sort((a, b) => a.
|
|
14613
|
+
chatTriggers.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
14567
14614
|
let prevCreatedAt = null;
|
|
14568
14615
|
for (const trigger of chatTriggers) {
|
|
14569
|
-
const preceding = (await tx.
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
LIMIT ${50}
|
|
14583
|
-
FOR UPDATE OF ie SKIP LOCKED
|
|
14584
|
-
`)).map((r) => ({
|
|
14585
|
-
id: r.message_id,
|
|
14586
|
-
senderId: r.sender_id,
|
|
14616
|
+
const preceding = (await tx.select({
|
|
14617
|
+
messageId: messages.id,
|
|
14618
|
+
senderId: messages.senderId,
|
|
14619
|
+
format: messages.format,
|
|
14620
|
+
content: messages.content,
|
|
14621
|
+
metadata: messages.metadata,
|
|
14622
|
+
createdAt: messages.createdAt
|
|
14623
|
+
}).from(inboxEntries).innerJoin(messages, eq(messages.id, inboxEntries.messageId)).where(and(eq(inboxEntries.inboxId, inboxId), eq(inboxEntries.chatId, chatId), eq(inboxEntries.status, "pending"), eq(inboxEntries.notify, false), lt(inboxEntries.createdAt, trigger.createdAt), prevCreatedAt === null ? void 0 : gt(inboxEntries.createdAt, prevCreatedAt), sql`${inboxEntries.createdAt} > NOW() - make_interval(secs => ${PRECEDING_CONTEXT_WINDOW_SECONDS})`)).orderBy(desc(inboxEntries.createdAt)).limit(50).for("update", {
|
|
14624
|
+
of: inboxEntries,
|
|
14625
|
+
skipLocked: true
|
|
14626
|
+
})).map((r) => ({
|
|
14627
|
+
id: r.messageId,
|
|
14628
|
+
senderId: r.senderId,
|
|
14587
14629
|
format: r.format,
|
|
14588
14630
|
content: r.content,
|
|
14589
14631
|
metadata: r.metadata ?? {},
|
|
14590
|
-
createdAt: r.
|
|
14632
|
+
createdAt: r.createdAt.toISOString()
|
|
14591
14633
|
})).reverse();
|
|
14592
14634
|
result.set(trigger.id, preceding);
|
|
14593
|
-
prevCreatedAt = trigger.
|
|
14635
|
+
prevCreatedAt = trigger.createdAt;
|
|
14594
14636
|
}
|
|
14595
14637
|
const latestTrigger = chatTriggers[chatTriggers.length - 1];
|
|
14596
|
-
if (latestTrigger) await tx.
|
|
14597
|
-
|
|
14598
|
-
|
|
14599
|
-
|
|
14600
|
-
AND chat_id = ${chatId}
|
|
14601
|
-
AND status = 'pending'
|
|
14602
|
-
AND notify = false
|
|
14603
|
-
AND created_at < ${latestTrigger.created_at}
|
|
14604
|
-
`);
|
|
14638
|
+
if (latestTrigger) await tx.update(inboxEntries).set({
|
|
14639
|
+
status: "acked",
|
|
14640
|
+
ackedAt: /* @__PURE__ */ new Date()
|
|
14641
|
+
}).where(and(eq(inboxEntries.inboxId, inboxId), eq(inboxEntries.chatId, chatId), eq(inboxEntries.status, "pending"), eq(inboxEntries.notify, false), lt(inboxEntries.createdAt, latestTrigger.createdAt)));
|
|
14605
14642
|
}
|
|
14606
14643
|
return result;
|
|
14607
14644
|
}
|
|
@@ -14644,23 +14681,14 @@ async function renewEntry(db, entryId, inboxId) {
|
|
|
14644
14681
|
return entry;
|
|
14645
14682
|
}
|
|
14646
14683
|
async function resetTimedOutEntries(db, timeoutSeconds = DEFAULT_INBOX_TIMEOUT_SECONDS, maxRetries = DEFAULT_MAX_RETRY_COUNT) {
|
|
14647
|
-
const
|
|
14648
|
-
|
|
14649
|
-
|
|
14650
|
-
|
|
14651
|
-
|
|
14652
|
-
RETURNING id
|
|
14653
|
-
`);
|
|
14654
|
-
const failedResult = await db.execute(sql`
|
|
14655
|
-
UPDATE inbox_entries SET status = 'failed'
|
|
14656
|
-
WHERE status = 'delivered'
|
|
14657
|
-
AND delivered_at < NOW() - make_interval(secs => ${timeoutSeconds})
|
|
14658
|
-
AND retry_count >= ${maxRetries}
|
|
14659
|
-
RETURNING id
|
|
14660
|
-
`);
|
|
14684
|
+
const reset = await db.update(inboxEntries).set({
|
|
14685
|
+
status: "pending",
|
|
14686
|
+
retryCount: sql`${inboxEntries.retryCount} + 1`
|
|
14687
|
+
}).where(and(eq(inboxEntries.status, "delivered"), sql`${inboxEntries.deliveredAt} < NOW() - make_interval(secs => ${timeoutSeconds})`, lt(inboxEntries.retryCount, maxRetries))).returning({ id: inboxEntries.id });
|
|
14688
|
+
const failed = await db.update(inboxEntries).set({ status: "failed" }).where(and(eq(inboxEntries.status, "delivered"), sql`${inboxEntries.deliveredAt} < NOW() - make_interval(secs => ${timeoutSeconds})`, gte(inboxEntries.retryCount, maxRetries))).returning({ id: inboxEntries.id });
|
|
14661
14689
|
return {
|
|
14662
|
-
reset:
|
|
14663
|
-
failed:
|
|
14690
|
+
reset: reset.length,
|
|
14691
|
+
failed: failed.length
|
|
14664
14692
|
};
|
|
14665
14693
|
}
|
|
14666
14694
|
/** Default age (30 days) past which silent rows that no notify-true delivery
|
|
@@ -14679,7 +14707,7 @@ const SILENT_ROW_GC_MAX_AGE_SECONDS = 720 * 60 * 60;
|
|
|
14679
14707
|
* `(inbox_id, message_id, chat_id)` means leaving them around blocks
|
|
14680
14708
|
* legitimate retries with the same key.
|
|
14681
14709
|
*
|
|
14682
|
-
* 2. `notify=false AND status='pending' AND
|
|
14710
|
+
* 2. `notify=false AND status='pending' AND createdAt < NOW() - maxAge` —
|
|
14683
14711
|
* stale silent rows that no trigger ever caught up with. After 30
|
|
14684
14712
|
* days they're useless as preceding context (the @mention almost
|
|
14685
14713
|
* certainly already happened or the chat went dormant).
|
|
@@ -14688,22 +14716,11 @@ const SILENT_ROW_GC_MAX_AGE_SECONDS = 720 * 60 * 60;
|
|
|
14688
14716
|
* can log meaningful counts.
|
|
14689
14717
|
*/
|
|
14690
14718
|
async function pruneStaleSilentEntries(db, maxAgeSeconds = SILENT_ROW_GC_MAX_AGE_SECONDS) {
|
|
14691
|
-
const
|
|
14692
|
-
|
|
14693
|
-
WHERE notify = false
|
|
14694
|
-
AND status = 'acked'
|
|
14695
|
-
RETURNING id
|
|
14696
|
-
`);
|
|
14697
|
-
const staleResult = await db.execute(sql`
|
|
14698
|
-
DELETE FROM inbox_entries
|
|
14699
|
-
WHERE notify = false
|
|
14700
|
-
AND status = 'pending'
|
|
14701
|
-
AND created_at < NOW() - make_interval(secs => ${maxAgeSeconds})
|
|
14702
|
-
RETURNING id
|
|
14703
|
-
`);
|
|
14719
|
+
const ackedDeleted = await db.delete(inboxEntries).where(and(eq(inboxEntries.notify, false), eq(inboxEntries.status, "acked"))).returning({ id: inboxEntries.id });
|
|
14720
|
+
const stalePendingDeleted = await db.delete(inboxEntries).where(and(eq(inboxEntries.notify, false), eq(inboxEntries.status, "pending"), sql`${inboxEntries.createdAt} < NOW() - make_interval(secs => ${maxAgeSeconds})`)).returning({ id: inboxEntries.id });
|
|
14704
14721
|
return {
|
|
14705
|
-
ackedDeleted:
|
|
14706
|
-
stalePendingDeleted:
|
|
14722
|
+
ackedDeleted: ackedDeleted.length,
|
|
14723
|
+
stalePendingDeleted: stalePendingDeleted.length
|
|
14707
14724
|
};
|
|
14708
14725
|
}
|
|
14709
14726
|
async function agentInboxRoutes(app) {
|
|
@@ -15581,8 +15598,9 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
15581
15598
|
}
|
|
15582
15599
|
boundAgents.clear();
|
|
15583
15600
|
if (clientId) {
|
|
15601
|
+
const stillActive = isActiveClientConnection(clientId, socket);
|
|
15584
15602
|
removeClientConnection(clientId, socket);
|
|
15585
|
-
try {
|
|
15603
|
+
if (stillActive) try {
|
|
15586
15604
|
await disconnectClient(app.db, clientId);
|
|
15587
15605
|
} catch {}
|
|
15588
15606
|
}
|