@helyx/bot 0.4.7 → 0.4.9

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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.9
4
+
5
+ ### Patch Changes
6
+
7
+ - Polish the Stage 4-6 resource dashboard and Discord flows, add durable
8
+ reaction-role controls, repair poll and giveaway lifecycle handling, and
9
+ strengthen shard-safe scheduling, retention, permissions and resource
10
+ inspection.
11
+ - Updated dependencies
12
+ - @helyx/control-protocol@0.1.5
13
+ - @helyx/module-manifest@0.4.2
14
+ - @helyx/database@0.4.3
15
+ - @helyx/sdk@0.3.4
16
+ - @helyx/platform@0.2.7
17
+ - @helyx/core@0.1.9
18
+ - @helyx/framework@0.1.9
19
+
20
+ ## 0.4.8
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies
25
+ - @helyx/platform@0.2.6
26
+
3
27
  ## 0.4.7
4
28
 
5
29
  ### Patch Changes
@@ -10,6 +10,7 @@ import { SHARD_HEALTH_STALE_MS, } from "./shard-contracts.js";
10
10
  const snowflake = z.string().regex(/^\d{17,20}$/u);
11
11
  const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
12
12
  const configurationValue = z.record(z.string(), z.union([
13
+ z.null(),
13
14
  z.string(),
14
15
  z.boolean(),
15
16
  z.number().finite(),
@@ -1099,7 +1100,7 @@ export async function createBotControlServer(input) {
1099
1100
  const body = mutationContext
1100
1101
  .extend({
1101
1102
  expectedRevision: z.number().int().min(0),
1102
- value: managedResourceObject,
1103
+ value: managedResourceObject.optional().default({}),
1103
1104
  })
1104
1105
  .strict()
1105
1106
  .parse(request.body);
@@ -21,7 +21,7 @@ export class DiscordOwnedMessageDeletionService {
21
21
  return { cleared: false };
22
22
  const reaction = message.reactions.resolve(emoji.cacheKey);
23
23
  if (!reaction)
24
- return { cleared: false };
24
+ return { cleared: true };
25
25
  await reaction.remove();
26
26
  return { cleared: true };
27
27
  }
@@ -108,6 +108,14 @@ function prepareEmojiIdentifier(value) {
108
108
  if (value.length < 1 || value.length > 128 || /[\r\n]/u.test(value)) {
109
109
  throw new Error("Invalid bounded Discord emoji identifier");
110
110
  }
111
+ const canonicalCustom = /^custom:(\d{17,20})$/u.exec(value);
112
+ if (canonicalCustom) {
113
+ const id = canonicalCustom[1];
114
+ return { identifier: id, cacheKey: id };
115
+ }
116
+ const canonicalUnicode = /^unicode:(.+)$/u.exec(value);
117
+ if (canonicalUnicode)
118
+ value = canonicalUnicode[1];
111
119
  const custom = /^(?:<a?:([A-Za-z0-9_]{2,32}):(\d{17,20})>|([A-Za-z0-9_]{2,32}):(\d{17,20}))$/u.exec(value);
112
120
  if (custom) {
113
121
  const name = custom[1] ?? custom[3];
package/dist/events.js CHANGED
@@ -574,9 +574,13 @@ export class DiscordResourceInspection {
574
574
  const guild = await this.client.guilds.fetch(input.guildId);
575
575
  if (guild.id !== input.guildId)
576
576
  throw new Error("Discord guild validation failed");
577
+ const canonicalCustom = /^custom:(\d{17,20})$/u.exec(input.emojiIdentifier);
578
+ const canonicalUnicode = /^unicode:(.+)$/u.exec(input.emojiIdentifier);
577
579
  const custom = customEmojiIdentifier.exec(input.emojiIdentifier);
578
- if (!custom) {
579
- const normalized = input.emojiIdentifier.normalize("NFKC").trim();
580
+ if (!custom && !canonicalCustom) {
581
+ const normalized = (canonicalUnicode?.[1] ?? input.emojiIdentifier)
582
+ .normalize("NFKC")
583
+ .trim();
580
584
  const valid = isSingleUnicodeEmoji(normalized);
581
585
  return {
582
586
  guildId: guild.id,
@@ -590,7 +594,9 @@ export class DiscordResourceInspection {
590
594
  : {}),
591
595
  };
592
596
  }
593
- const emojiId = custom[2] ?? custom[4];
597
+ const emojiId = canonicalCustom?.[1] ?? custom?.[2] ?? custom?.[4];
598
+ if (!emojiId)
599
+ throw new Error("Invalid bounded Discord emoji identifier");
594
600
  const emoji = await guild.emojis.fetch(emojiId).catch(() => null);
595
601
  if (!emoji)
596
602
  return {
@@ -612,7 +618,7 @@ export class DiscordResourceInspection {
612
618
  kind: "custom",
613
619
  canonicalEmojiKey: `custom:${emoji.id}`,
614
620
  emojiId: emoji.id,
615
- name: emoji.name ?? custom[1] ?? custom[3] ?? "emoji",
621
+ name: emoji.name ?? custom?.[1] ?? custom?.[3] ?? "emoji",
616
622
  };
617
623
  }
618
624
  async inspectChannel(input) {
@@ -67,7 +67,24 @@ export function installInteractions(client, modules, registration, logger, servi
67
67
  });
68
68
  async function handleInteraction(interaction) {
69
69
  try {
70
- if (interaction.isChatInputCommand()) {
70
+ if (interaction.isAutocomplete()) {
71
+ const registered = commands.get(interaction.commandName);
72
+ const subcommandName = registered?.contribution.subcommands?.length
73
+ ? interaction.options.getSubcommand(false)
74
+ : null;
75
+ const subcommand = subcommandName
76
+ ? registered?.contribution.subcommands?.find(({ name }) => name === subcommandName)
77
+ : undefined;
78
+ if (!registered ||
79
+ !subcommand?.autocomplete ||
80
+ !(await authorizeAutocomplete(interaction, registered.moduleId, subcommand.permissionId ??
81
+ `${registered.contribution.name}.${subcommand.name}`, services, registration.selfAuthorizingGuildModuleIds?.has(registered.moduleId) ?? false, subcommand.authorization === "resource-scoped"))) {
82
+ await interaction.respond([]);
83
+ return;
84
+ }
85
+ await subcommand.autocomplete(createAutocompleteContext(interaction, services));
86
+ }
87
+ else if (interaction.isChatInputCommand()) {
71
88
  const registered = commands.get(interaction.commandName);
72
89
  if (registered) {
73
90
  const { contribution: command, moduleId } = registered;
@@ -217,6 +234,8 @@ export function toCommandDefinition(registered) {
217
234
  }
218
235
  function addCommandOption(builder, option) {
219
236
  if (option.type === "string") {
237
+ if (option.autocomplete && option.choices?.length)
238
+ throw new Error(`Command option ${option.name} cannot use choices and autocomplete together`);
220
239
  builder.addStringOption((optionBuilder) => {
221
240
  optionBuilder
222
241
  .setName(option.name)
@@ -228,6 +247,8 @@ function addCommandOption(builder, option) {
228
247
  optionBuilder.setMaxLength(option.maxLength);
229
248
  if (option.choices)
230
249
  optionBuilder.addChoices(...option.choices);
250
+ if (option.autocomplete)
251
+ optionBuilder.setAutocomplete(option.autocomplete);
231
252
  return optionBuilder;
232
253
  });
233
254
  return;
@@ -335,6 +356,41 @@ function createCommandContext(interaction, services) {
335
356
  },
336
357
  };
337
358
  }
359
+ function createAutocompleteContext(interaction, services) {
360
+ const focused = interaction.options.getFocused(true);
361
+ const actor = permissionActor(interaction);
362
+ return {
363
+ interactionId: interaction.id,
364
+ userId: actor.userId,
365
+ userRoleIds: actor.roleIds,
366
+ userPermissions: actor.permissions,
367
+ isServerOwner: actor.isServerOwner,
368
+ isAdministrator: actor.isAdministrator,
369
+ ...(interaction.guildId ? { serverId: interaction.guildId } : {}),
370
+ focusedOptionName: focused.name,
371
+ focusedValue: String(focused.value),
372
+ services,
373
+ respond: async (choices) => {
374
+ if (choices.length > 25)
375
+ throw new Error("Autocomplete supports at most 25 choices");
376
+ const values = new Set();
377
+ const normalized = choices.map(({ name, value }) => {
378
+ const choiceName = name.trim().normalize("NFC");
379
+ const choiceValue = value.trim().normalize("NFC");
380
+ if (choiceName.length < 1 ||
381
+ choiceName.length > 100 ||
382
+ choiceValue.length < 1 ||
383
+ choiceValue.length > 100)
384
+ throw new Error("Autocomplete names and values must be between 1 and 100 characters");
385
+ if (values.has(choiceValue))
386
+ throw new Error("Autocomplete choice values must be unique");
387
+ values.add(choiceValue);
388
+ return { name: choiceName, value: choiceValue };
389
+ });
390
+ await interaction.respond(normalized);
391
+ },
392
+ };
393
+ }
338
394
  function createModalContext(interaction, services) {
339
395
  const updateResponse = interaction.isFromMessage()
340
396
  ? async (response) => interaction.update(toUpdateOptions(response)).then(() => undefined)
@@ -467,17 +523,7 @@ export async function authorizeCommand(interaction, moduleId, commandId, rateLim
467
523
  if (!resourceScopedCommand) {
468
524
  const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
469
525
  const policy = await permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId);
470
- const actor = {
471
- userId: interaction.user.id,
472
- roleIds: interaction.member instanceof GuildMember
473
- ? [...interaction.member.roles.cache.keys()]
474
- : (interaction.member?.roles ?? []),
475
- permissions: interaction.memberPermissions
476
- ?.toArray()
477
- .map(normalizeDiscordPermission) ?? [],
478
- isServerOwner: interaction.guild?.ownerId === interaction.user.id,
479
- isAdministrator: interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ?? false,
480
- };
526
+ const actor = permissionActor(interaction);
481
527
  if (!canUseCommand(actor, policy)) {
482
528
  await interaction.reply({
483
529
  content: "You do not have permission to use this command.",
@@ -506,6 +552,32 @@ export async function authorizeCommand(interaction, moduleId, commandId, rateLim
506
552
  }
507
553
  return true;
508
554
  }
555
+ async function authorizeAutocomplete(interaction, moduleId, commandId, services, selfAuthorizingGuildCommand, resourceScopedCommand) {
556
+ if (!interaction.guildId || selfAuthorizingGuildCommand)
557
+ return true;
558
+ const installations = services.get(HELYX_SERVICE_NAMES.installations);
559
+ if (!(await installations.isModuleEnabled(interaction.guildId, moduleId)))
560
+ return false;
561
+ if (resourceScopedCommand)
562
+ return true;
563
+ const permissions = services.get(HELYX_SERVICE_NAMES.permissions);
564
+ const policy = await permissions.getEffectivePolicy(interaction.guildId, moduleId, commandId);
565
+ return canUseCommand(permissionActor(interaction), policy);
566
+ }
567
+ function permissionActor(interaction) {
568
+ return {
569
+ userId: interaction.user.id,
570
+ roleIds: interaction.member instanceof GuildMember
571
+ ? [...interaction.member.roles.cache.keys()]
572
+ : (interaction.member?.roles ?? []),
573
+ permissions: interaction.memberPermissions
574
+ ?.toArray()
575
+ .map(normalizeDiscordPermission) ?? [],
576
+ isServerOwner: interaction.guild?.ownerId === interaction.user.id,
577
+ isAdministrator: interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ??
578
+ false,
579
+ };
580
+ }
509
581
  export function normalizeDiscordPermission(permission) {
510
582
  return permission.replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
511
583
  }
@@ -760,6 +832,10 @@ export function toModal(definition) {
760
832
  return modal;
761
833
  }
762
834
  async function sendFailure(interaction) {
835
+ if (interaction.isAutocomplete()) {
836
+ await interaction.respond([]).catch(() => undefined);
837
+ return;
838
+ }
763
839
  if (!interaction.isRepliable())
764
840
  return;
765
841
  const message = "Something went wrong while handling that interaction.";
@@ -306,11 +306,17 @@ export function createLocalScheduledTaskRoute(input) {
306
306
  .execute(retentionTaskId);
307
307
  return result.completed
308
308
  ? { outcome: "completed" }
309
- : {
310
- outcome: "deferred",
311
- delayMs: 100,
312
- safeCode: "retention_batch_remaining",
313
- };
309
+ : result.deferredUntil
310
+ ? {
311
+ outcome: "deferred",
312
+ delayMs: Math.max(100, Math.min(86_400_000, result.deferredUntil.getTime() - Date.now())),
313
+ safeCode: "retention_rescheduled",
314
+ }
315
+ : {
316
+ outcome: "deferred",
317
+ delayMs: 100,
318
+ safeCode: "retention_batch_remaining",
319
+ };
314
320
  }
315
321
  const module = modules.get(task.moduleId);
316
322
  if (!module)
@@ -3,7 +3,7 @@ import type { ManagedResourceValue } from "@helyx/sdk";
3
3
  import type { BotControlHandler } from "./control-server.js";
4
4
  export declare const SHARD_CONTRACT_VERSION: 1;
5
5
  export declare const SHARD_CONTROL_MAX_BYTES: number;
6
- export declare const SHARD_CONTROL_TIMEOUT_MS = 10000;
6
+ export declare const SHARD_CONTROL_TIMEOUT_MS = 30000;
7
7
  export declare const SHARD_HEALTH_STALE_MS = 30000;
8
8
  export declare const SHARD_LEASE_HEARTBEAT_TIMEOUT_MS = 20000;
9
9
  export declare const shardIdentitySchema: z.ZodObject<{
@@ -182,7 +182,7 @@ export declare const shardControlOperationSchema: z.ZodDiscriminatedUnion<[z.Zod
182
182
  kind: z.ZodLiteral<"guild.module.configuration.update">;
183
183
  guildId: z.ZodString;
184
184
  moduleId: z.ZodString;
185
- value: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
185
+ value: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodNull, z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
186
186
  expectedVersion: z.ZodNullable<z.ZodNumber>;
187
187
  }, z.core.$strict>, z.ZodObject<{
188
188
  actorUserId: z.ZodString;
@@ -380,7 +380,7 @@ export declare const shardControlRequestSchema: z.ZodObject<{
380
380
  kind: z.ZodLiteral<"guild.module.configuration.update">;
381
381
  guildId: z.ZodString;
382
382
  moduleId: z.ZodString;
383
- value: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
383
+ value: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodNull, z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
384
384
  expectedVersion: z.ZodNullable<z.ZodNumber>;
385
385
  }, z.core.$strict>, z.ZodObject<{
386
386
  actorUserId: z.ZodString;
@@ -838,7 +838,7 @@ export declare const supervisorMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObje
838
838
  kind: z.ZodLiteral<"guild.module.configuration.update">;
839
839
  guildId: z.ZodString;
840
840
  moduleId: z.ZodString;
841
- value: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
841
+ value: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodNull, z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
842
842
  expectedVersion: z.ZodNullable<z.ZodNumber>;
843
843
  }, z.core.$strict>, z.ZodObject<{
844
844
  actorUserId: z.ZodString;
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  export const SHARD_CONTRACT_VERSION = 1;
3
3
  export const SHARD_CONTROL_MAX_BYTES = 64 * 1_024;
4
- export const SHARD_CONTROL_TIMEOUT_MS = 10_000;
4
+ export const SHARD_CONTROL_TIMEOUT_MS = 30_000;
5
5
  export const SHARD_HEALTH_STALE_MS = 30_000;
6
6
  export const SHARD_LEASE_HEARTBEAT_TIMEOUT_MS = 20_000;
7
7
  const snowflake = z.string().regex(/^\d{17,20}$/u);
@@ -43,6 +43,7 @@ const permissionState = z
43
43
  })
44
44
  .strict();
45
45
  const configurationValue = z.record(z.string().min(1).max(100), z.union([
46
+ z.null(),
46
47
  z.string().max(4_000),
47
48
  z.boolean(),
48
49
  z.number().finite(),
@@ -5,6 +5,9 @@ import { type ShardCloudRequest, type ShardIdentity, type ShardProcessHealth } f
5
5
  import { type ShardCoordinator, type ShardGenerationDescriptor, type ShardLeaderLease, type ShardLease } from "./shard-coordinator.js";
6
6
  import type { BotHandle } from "./index.js";
7
7
  export declare const STEADY_STATE_LEASE_LOSS_BOUND_MS: number;
8
+ export declare function createStartupWorkerHeartbeatMaintenance(heartbeat: () => void, intervalMs?: number): {
9
+ stop(): void;
10
+ };
8
11
  export declare function deferScheduledRoutingFailures(route: () => Promise<ScheduledTaskExecutionResult>): Promise<ScheduledTaskExecutionResult>;
9
12
  export declare function startShardSupervisor(environment: BotEnvironment, processEnvironment: NodeJS.ProcessEnv, workerEntry: string): Promise<BotHandle>;
10
13
  export declare function buildShardWorkerEnvironment(source: NodeJS.ProcessEnv, identity: ShardIdentity, restartCount: number): NodeJS.ProcessEnv;
@@ -31,6 +31,13 @@ const WORKER_SHUTDOWN_MS = 5_000;
31
31
  const MAX_RESTARTS_PER_WINDOW = 5;
32
32
  const RESTART_WINDOW_MS = 10 * 60 * 1_000;
33
33
  export const STEADY_STATE_LEASE_LOSS_BOUND_MS = LEASE_RENEW_INTERVAL_MS + LEASE_VALIDATION_TIMEOUT_MS + WORKER_SHUTDOWN_MS;
34
+ export function createStartupWorkerHeartbeatMaintenance(heartbeat, intervalMs = STARTUP_LEASE_RENEW_INTERVAL_MS) {
35
+ const timer = setInterval(heartbeat, intervalMs);
36
+ timer.unref();
37
+ return {
38
+ stop: () => clearInterval(timer),
39
+ };
40
+ }
34
41
  export async function deferScheduledRoutingFailures(route) {
35
42
  try {
36
43
  return await route();
@@ -73,6 +80,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
73
80
  let retentionTimer;
74
81
  let leaseTimer;
75
82
  let startupLeaseTimer;
83
+ let startupWorkerHeartbeats;
76
84
  let startupLeaseRefresh;
77
85
  let startupLeaseError;
78
86
  let shuttingDown = false;
@@ -297,6 +305,11 @@ export async function startShardSupervisor(environment, processEnvironment, work
297
305
  execArgv: process.execArgv,
298
306
  });
299
307
  await reserveInitialSessionStarts(claimedIds.length, gateway);
308
+ if (coordinator) {
309
+ startupWorkerHeartbeats = createStartupWorkerHeartbeatMaintenance(() => {
310
+ void Promise.all([...localLeases].map(([shardId, lease]) => sendLeaseHeartbeat(shardId, lease)));
311
+ });
312
+ }
300
313
  await spawnInIdentifyBuckets(claimedIds, gateway.session_start_limit.max_concurrency);
301
314
  workersRoutable = true;
302
315
  if (leaderLease)
@@ -317,8 +330,11 @@ export async function startShardSupervisor(environment, processEnvironment, work
317
330
  await stopStartupLeaseMaintenance();
318
331
  if (coordinator)
319
332
  await withTimeout(renewStartupOwnership(coordinator, generationDescriptor, deploymentOrder, leaderLease), LEASE_VALIDATION_TIMEOUT_MS);
333
+ await Promise.all([...localLeases].map(([shardId, lease]) => sendLeaseHeartbeat(shardId, lease)));
320
334
  leaseTimer = setInterval(() => void maintainLeases(), LEASE_RENEW_INTERVAL_MS);
321
335
  leaseTimer.unref();
336
+ startupWorkerHeartbeats?.stop();
337
+ startupWorkerHeartbeats = undefined;
322
338
  process.once("SIGINT", () => void shutdown("SIGINT"));
323
339
  process.once("SIGTERM", () => void shutdown("SIGTERM"));
324
340
  logger.info({
@@ -414,17 +430,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
414
430
  };
415
431
  shard.env = buildShardWorkerEnvironment(processEnvironment, identity, restartCounts.get(shardId) ?? 0);
416
432
  healthByShard.set(shardId, startingHealth(shardId, restartCounts.get(shardId) ?? 0));
417
- const launchHeartbeatTimer = coordinator
418
- ? setInterval(() => void sendLeaseHeartbeat(shardId, lease), STARTUP_LEASE_RENEW_INTERVAL_MS)
419
- : undefined;
420
- launchHeartbeatTimer?.unref();
421
- try {
422
- await spawnWithShutdownGuard(() => shuttingDown, () => shard.spawn(30_000), () => shard.kill());
423
- }
424
- finally {
425
- if (launchHeartbeatTimer)
426
- clearInterval(launchHeartbeatTimer);
427
- }
433
+ await spawnWithShutdownGuard(() => shuttingDown, () => shard.spawn(30_000), () => shard.kill());
428
434
  await sendLeaseHeartbeat(shardId, lease);
429
435
  }
430
436
  async function sendLeaseHeartbeat(shardId, lease) {
@@ -985,6 +991,8 @@ export async function startShardSupervisor(environment, processEnvironment, work
985
991
  clearInterval(leaseTimer);
986
992
  if (startupLeaseTimer)
987
993
  clearInterval(startupLeaseTimer);
994
+ startupWorkerHeartbeats?.stop();
995
+ startupWorkerHeartbeats = undefined;
988
996
  if (retentionTimer)
989
997
  clearInterval(retentionTimer);
990
998
  workersRoutable = false;
@@ -207,7 +207,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
207
207
  if (!parsed.success)
208
208
  return;
209
209
  if (parsed.data.type === "helyx.control.request") {
210
- void handleControl(identity, controlService, parsed.data);
210
+ void handleControl(identity, controlService, parsed.data, logger);
211
211
  }
212
212
  else if (parsed.data.type === "helyx.lease.heartbeat") {
213
213
  if (sameShardIdentity(parsed.data.identity, identity))
@@ -390,7 +390,7 @@ export function shardWorkerCommandRegistration(input) {
390
390
  ]),
391
391
  };
392
392
  }
393
- async function handleControl(identity, service, request) {
393
+ async function handleControl(identity, service, request, logger) {
394
394
  let response;
395
395
  const guildId = controlOperationGuildId(request.operation);
396
396
  if (request.generation !== identity.generation ||
@@ -418,6 +418,13 @@ async function handleControl(identity, service, request) {
418
418
  };
419
419
  }
420
420
  catch (error) {
421
+ logger.error({
422
+ event: "shard.control_failed",
423
+ requestId: request.requestId,
424
+ operation: request.operation.kind,
425
+ guildId,
426
+ error: safeError(error),
427
+ }, "Shard control operation failed");
421
428
  const mapped = mapBotControlError(error);
422
429
  response = {
423
430
  version: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/bot",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Production Discord runtime for the modular Helyx platform.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -49,13 +49,13 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@helyx/config": "0.4.2",
52
- "@helyx/control-protocol": "0.1.4",
53
- "@helyx/core": "0.1.8",
54
- "@helyx/database": "0.4.2",
55
- "@helyx/framework": "0.1.8",
56
- "@helyx/module-manifest": "0.4.1",
57
- "@helyx/platform": "0.2.5",
58
- "@helyx/sdk": "0.3.3",
52
+ "@helyx/control-protocol": "0.1.5",
53
+ "@helyx/core": "0.1.9",
54
+ "@helyx/database": "0.4.3",
55
+ "@helyx/framework": "0.1.9",
56
+ "@helyx/module-manifest": "0.4.2",
57
+ "@helyx/platform": "0.2.7",
58
+ "@helyx/sdk": "0.3.4",
59
59
  "discord.js": "14.27.0",
60
60
  "fastify": "5.10.0",
61
61
  "pino": "10.3.1",