@helyx/bot 0.4.40 → 0.4.41

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.41
4
+
5
+ ### Patch Changes
6
+
7
+ - Keep coordinated rolling deployments ready through remote shard ownership and atomically escalate terminal privacy-provider failures to staff review.
8
+
3
9
  ## 0.4.40
4
10
 
5
11
  ### Patch Changes
@@ -559,6 +559,7 @@ export declare function createBotControlServer(input: {
559
559
  level: string;
560
560
  };
561
561
  shardHealth?: () => Promise<ShardHealthSnapshot>;
562
+ readiness?: () => Promise<boolean>;
562
563
  guildCommands?: Parameters<typeof registerGuildCommandControlRoutes>[1];
563
564
  }): Promise<FastifyInstance>;
564
565
  export declare function mapBotControlError(error: unknown): {
@@ -1209,10 +1209,18 @@ export async function createBotControlServer(input) {
1209
1209
  return reply.code(401).send({ message: "Control authentication failed" });
1210
1210
  });
1211
1211
  async function readiness(reply) {
1212
- if (!input.shardHealth)
1212
+ const deploymentReadiness = input.readiness;
1213
+ const shardHealth = input.shardHealth;
1214
+ if (!deploymentReadiness && !shardHealth)
1213
1215
  return { status: "ok" };
1214
1216
  try {
1215
- const health = await input.shardHealth();
1217
+ if (deploymentReadiness && (await deploymentReadiness()))
1218
+ return { status: "ok" };
1219
+ if (deploymentReadiness)
1220
+ return reply.code(503).send({ status: "unavailable" });
1221
+ if (!shardHealth)
1222
+ return reply.code(503).send({ status: "unavailable" });
1223
+ const health = await shardHealth();
1216
1224
  if (health.status === "healthy")
1217
1225
  return { status: "ok" };
1218
1226
  }
@@ -2,7 +2,7 @@ import type { BotEnvironment } from "@helyx/config";
2
2
  import { type DatabasePool } from "@helyx/database";
3
3
  import type { ModuleDefinition, NumericWriteBehindCommittedBatch, ScheduledTaskExecutionResult } from "@helyx/sdk";
4
4
  import { buildApplicationCommandDefinitions } from "./interactions.js";
5
- import { type ShardCloudRequest, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
5
+ import { type ShardCloudRequest, type ShardHealthSnapshot, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
6
6
  import { type ShardCoordinator, type ShardGenerationDescriptor, type ShardLeaderLease, type ShardLease } from "./shard-coordinator.js";
7
7
  import type { BotHandle } from "./index.js";
8
8
  export declare const STEADY_STATE_LEASE_LOSS_BOUND_MS: number;
@@ -20,6 +20,11 @@ export declare function startShardSupervisor(environment: BotEnvironment, proces
20
20
  export declare function splitNumericCommittedBatch(batch: NumericWriteBehindCommittedBatch, index: number): NumericWriteBehindCommittedBatch;
21
21
  export declare function buildShardWorkerEnvironment(source: NodeJS.ProcessEnv, identity: ShardIdentity, restartCount: number): NodeJS.ProcessEnv;
22
22
  export declare function shardRuntimeCompositionHash(modules: readonly ModuleDefinition[], internalGuildId?: string, globalCommandMode?: Parameters<typeof buildApplicationCommandDefinitions>[2]): string;
23
+ export declare function coordinatedFollowerReady(input: {
24
+ health: ShardHealthSnapshot;
25
+ activeLeader: ShardLeaderLease | undefined;
26
+ supervisorId: string;
27
+ }): boolean;
23
28
  export declare function boundedRespawnDelay(attempt: number): number;
24
29
  export interface SteadyStateOwnershipRenewal {
25
30
  generationRenewed: boolean;
@@ -420,6 +420,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
420
420
  secret: environment.HELYX_BOT_CONTROL_SECRET,
421
421
  logger: { level: environment.LOG_LEVEL },
422
422
  shardHealth: buildHealth,
423
+ readiness: buildDeploymentReadiness,
423
424
  guildCommands: new GuildCommandReadService(database, environment.DISCORD_CLIENT_ID, () => guildCommandWorker?.getHealth(), environment.DISCORD_TOKEN, environment.HELYX_GUILD_COMMAND_WRITES_ENABLED, environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS, environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all", commandModules),
424
425
  });
425
426
  await controlServer.listen({
@@ -1343,6 +1344,19 @@ export async function startShardSupervisor(environment, processEnvironment, work
1343
1344
  shards,
1344
1345
  });
1345
1346
  }
1347
+ async function buildDeploymentReadiness() {
1348
+ const health = await buildHealth();
1349
+ if (health.status === "healthy")
1350
+ return true;
1351
+ if (!coordinator)
1352
+ return false;
1353
+ const activeLeader = await withTimeout(coordinator.getLeaderLease(), LEASE_VALIDATION_TIMEOUT_MS).catch(() => undefined);
1354
+ return coordinatedFollowerReady({
1355
+ health,
1356
+ activeLeader,
1357
+ supervisorId,
1358
+ });
1359
+ }
1346
1360
  async function shutdown(reason = "programmatic") {
1347
1361
  if (shuttingDown)
1348
1362
  return;
@@ -1690,6 +1704,16 @@ function boundedOperationalHealth(health) {
1690
1704
  shards: health.shards.slice(0, 25),
1691
1705
  });
1692
1706
  }
1707
+ export function coordinatedFollowerReady(input) {
1708
+ return (input.health.scope === "coordinated" &&
1709
+ input.health.status === "degraded" &&
1710
+ input.health.readyShardCount === input.health.configuredShardCount &&
1711
+ input.health.coordination.redis === "healthy" &&
1712
+ input.health.coordination.database === "healthy" &&
1713
+ input.health.scheduler?.status === "unavailable" &&
1714
+ input.activeLeader?.generation === input.health.generation &&
1715
+ input.activeLeader.supervisorId !== input.supervisorId);
1716
+ }
1693
1717
  function wait(milliseconds) {
1694
1718
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
1695
1719
  }
package/package.json CHANGED
@@ -1,74 +1,74 @@
1
- {
2
- "name": "@helyx/bot",
3
- "version": "0.4.40",
4
- "description": "Production Discord runtime for the modular Helyx platform.",
5
- "keywords": [
6
- "helyx",
7
- "discord",
8
- "bot",
9
- "modular"
10
- ],
11
- "private": false,
12
- "license": "HSAL 1.0",
13
- "homepage": "https://helyx.gg/",
14
- "bugs": {
15
- "url": "https://github.com/ZyC0R3/Helyx/issues"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/ZyC0R3/Helyx.git",
20
- "directory": "apps/bot"
21
- },
22
- "engines": {
23
- "node": ">=24 <27",
24
- "npm": ">=11"
25
- },
26
- "publishConfig": {
27
- "access": "public",
28
- "provenance": true
29
- },
30
- "type": "module",
31
- "files": [
32
- "dist",
33
- "!dist/*.tsbuildinfo",
34
- "!dist/**/*.map",
35
- "documentation",
36
- "CHANGELOG.md",
37
- "README.md",
38
- "LICENSE"
39
- ],
40
- "exports": {
41
- ".": {
42
- "types": "./dist/index.d.ts",
43
- "default": "./dist/index.js"
44
- }
45
- },
46
- "scripts": {
47
- "build": "tsc -b",
48
- "prepack": "node ../../scripts/public-documentation.mjs --sync-bot",
49
- "dev": "node --env-file-if-exists=../../.env --import tsx src/index.ts",
50
- "start": "node --env-file-if-exists=../../.env dist/index.js"
51
- },
52
- "dependencies": {
53
- "@helyx/config": ">=0.5.0 <0.6.0",
54
- "@helyx/control-protocol": ">=0.4.0 <0.5.0",
55
- "@helyx/core": ">=0.1.0 <0.2.0",
56
- "@helyx/database": ">=0.8.6 <0.9.0",
57
- "@helyx/framework": ">=0.1.0 <0.2.0",
58
- "@helyx/module-manifest": ">=0.7.0 <0.8.0",
59
- "@helyx/platform": ">=0.6.2 <0.7.0",
60
- "@helyx/sdk": ">=0.8.4 <0.9.0",
61
- "@helyx/status-client": ">=0.1.0 <0.2.0",
62
- "@helyx/verification-contracts": ">=0.3.0 <0.4.0",
63
- "discord.js": "14.27.0",
64
- "fastify": "5.10.0",
65
- "pino": "10.3.1",
66
- "redis": "6.1.0",
67
- "undici": "6.28.0",
68
- "ws": "8.21.1",
69
- "zod": "4.4.3"
70
- },
71
- "devDependencies": {
72
- "@types/ws": "8.18.1"
73
- }
74
- }
1
+ {
2
+ "name": "@helyx/bot",
3
+ "version": "0.4.41",
4
+ "description": "Production Discord runtime for the modular Helyx platform.",
5
+ "keywords": [
6
+ "helyx",
7
+ "discord",
8
+ "bot",
9
+ "modular"
10
+ ],
11
+ "private": false,
12
+ "license": "HSAL 1.0",
13
+ "homepage": "https://helyx.gg/",
14
+ "bugs": {
15
+ "url": "https://github.com/ZyC0R3/Helyx/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/ZyC0R3/Helyx.git",
20
+ "directory": "apps/bot"
21
+ },
22
+ "engines": {
23
+ "node": ">=24 <27",
24
+ "npm": ">=11"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public",
28
+ "provenance": true
29
+ },
30
+ "type": "module",
31
+ "files": [
32
+ "dist",
33
+ "!dist/*.tsbuildinfo",
34
+ "!dist/**/*.map",
35
+ "documentation",
36
+ "CHANGELOG.md",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "exports": {
41
+ ".": {
42
+ "types": "./dist/index.d.ts",
43
+ "default": "./dist/index.js"
44
+ }
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -b",
48
+ "prepack": "node ../../scripts/public-documentation.mjs --sync-bot",
49
+ "dev": "node --env-file-if-exists=../../.env --import tsx src/index.ts",
50
+ "start": "node --env-file-if-exists=../../.env dist/index.js"
51
+ },
52
+ "dependencies": {
53
+ "@helyx/config": ">=0.5.0 <0.6.0",
54
+ "@helyx/control-protocol": ">=0.4.0 <0.5.0",
55
+ "@helyx/core": ">=0.1.0 <0.2.0",
56
+ "@helyx/database": ">=0.8.6 <0.9.0",
57
+ "@helyx/framework": ">=0.1.0 <0.2.0",
58
+ "@helyx/module-manifest": ">=0.7.0 <0.8.0",
59
+ "@helyx/platform": ">=0.6.2 <0.7.0",
60
+ "@helyx/sdk": ">=0.8.4 <0.9.0",
61
+ "@helyx/status-client": ">=0.1.0 <0.2.0",
62
+ "@helyx/verification-contracts": ">=0.3.0 <0.4.0",
63
+ "discord.js": "14.27.0",
64
+ "fastify": "5.10.0",
65
+ "pino": "10.3.1",
66
+ "redis": "6.1.0",
67
+ "undici": "6.28.0",
68
+ "ws": "8.21.1",
69
+ "zod": "4.4.3"
70
+ },
71
+ "devDependencies": {
72
+ "@types/ws": "8.18.1"
73
+ }
74
+ }