@questionbase/deskfree 0.3.0-alpha.37 → 0.3.0-alpha.38

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/dist/index.js CHANGED
@@ -3651,6 +3651,132 @@ var require_websocket_server = __commonJS({
3651
3651
  }
3652
3652
  });
3653
3653
 
3654
+ // src/channel.validation.ts
3655
+ function validateField(opts) {
3656
+ const { value, name, minLength, maxLength, pattern, patternMessage } = opts;
3657
+ if (!value) return `${name} is required`;
3658
+ if (typeof value !== "string") return `${name} must be a string`;
3659
+ const trimmed = value.trim();
3660
+ if (trimmed !== value)
3661
+ return `${name} must not have leading or trailing whitespace`;
3662
+ if (minLength !== void 0 && trimmed.length < minLength)
3663
+ return `${name} appears to be incomplete (minimum ${minLength} characters expected)`;
3664
+ if (maxLength !== void 0 && trimmed.length > maxLength)
3665
+ return `${name} appears to be invalid (maximum ${maxLength} characters expected)`;
3666
+ if (pattern !== void 0 && !pattern.test(trimmed))
3667
+ return patternMessage ?? `${name} contains invalid characters`;
3668
+ return null;
3669
+ }
3670
+ function isLocalDevelopmentHost(hostname) {
3671
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.endsWith(".local") || hostname.endsWith(".localhost") || /^192\.168\./.test(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname);
3672
+ }
3673
+ function validateBotToken(value) {
3674
+ const fieldError = validateField({ value, name: "Bot token" });
3675
+ if (fieldError) return fieldError;
3676
+ const trimmed = value.trim();
3677
+ if (!trimmed.startsWith("bot_")) {
3678
+ return 'Bot token must start with "bot_" (check your DeskFree bot configuration)';
3679
+ }
3680
+ const patternError = validateField({
3681
+ value,
3682
+ name: "Bot token",
3683
+ minLength: 10,
3684
+ maxLength: 200,
3685
+ pattern: /^bot_[a-zA-Z0-9_-]+$/,
3686
+ patternMessage: 'Bot token contains invalid characters. Only alphanumeric, underscore, and dash are allowed after "bot_"'
3687
+ });
3688
+ if (patternError) return patternError;
3689
+ if (trimmed.includes(" ") || trimmed.includes("\n") || trimmed.includes(" ")) {
3690
+ return "Bot token contains whitespace characters. Please copy the token exactly as shown in DeskFree.";
3691
+ }
3692
+ if (trimmed === "bot_your_token_here" || trimmed === "bot_example") {
3693
+ return "Please replace the placeholder with your actual bot token from DeskFree";
3694
+ }
3695
+ return null;
3696
+ }
3697
+ function validateApiUrl(value) {
3698
+ const fieldError = validateField({ value, name: "API URL" });
3699
+ if (fieldError) return fieldError;
3700
+ const trimmed = value.trim();
3701
+ let url;
3702
+ try {
3703
+ url = new URL(trimmed);
3704
+ } catch (err) {
3705
+ const message = err instanceof Error ? err.message : "Invalid URL format";
3706
+ return `API URL must be a valid URL: ${message}`;
3707
+ }
3708
+ if (url.protocol !== "https:") {
3709
+ return "API URL must use HTTPS protocol for security. Make sure your DeskFree deployment supports HTTPS.";
3710
+ }
3711
+ if (!url.hostname) {
3712
+ return "API URL must have a valid hostname";
3713
+ }
3714
+ if (isLocalDevelopmentHost(url.hostname)) {
3715
+ if (process.env.NODE_ENV === "production") {
3716
+ return "API URL cannot use localhost or private IP addresses in production. Please use a publicly accessible URL.";
3717
+ }
3718
+ }
3719
+ if (url.hostname.includes("..") || url.hostname.startsWith(".")) {
3720
+ return "API URL hostname appears to be malformed. Please check for typos.";
3721
+ }
3722
+ return null;
3723
+ }
3724
+ function validateWebSocketUrl(value) {
3725
+ const fieldError = validateField({ value, name: "WebSocket URL" });
3726
+ if (fieldError) return fieldError;
3727
+ const trimmed = value.trim();
3728
+ let url;
3729
+ try {
3730
+ url = new URL(trimmed);
3731
+ } catch (err) {
3732
+ const message = err instanceof Error ? err.message : "Invalid URL format";
3733
+ return `WebSocket URL must be a valid URL: ${message}`;
3734
+ }
3735
+ if (!["ws:", "wss:"].includes(url.protocol)) {
3736
+ return "WebSocket URL must use ws:// or wss:// protocol";
3737
+ }
3738
+ if (!url.hostname) {
3739
+ return "WebSocket URL must have a valid hostname";
3740
+ }
3741
+ if (isLocalDevelopmentHost(url.hostname)) {
3742
+ if (process.env.NODE_ENV === "production") {
3743
+ return "WebSocket URL cannot use localhost or private IP addresses in production. Please use a publicly accessible URL.";
3744
+ }
3745
+ }
3746
+ if (url.hostname.includes("..") || url.hostname.startsWith(".")) {
3747
+ return "WebSocket URL hostname appears to be malformed. Please check for typos.";
3748
+ }
3749
+ return null;
3750
+ }
3751
+ function validateUserId(value) {
3752
+ const fieldError = validateField({ value, name: "User ID" });
3753
+ if (fieldError) return fieldError;
3754
+ const trimmed = value.trim();
3755
+ const normalizedUserId = trimmed.toUpperCase();
3756
+ if (normalizedUserId !== trimmed) {
3757
+ return "User ID should be uppercase (will be automatically converted)";
3758
+ }
3759
+ if (!/^[UBPT][A-Z0-9]{10}$/.test(normalizedUserId)) {
3760
+ if (normalizedUserId.length !== 11) {
3761
+ return `User ID must be exactly 11 characters long (got: ${normalizedUserId.length}). Example: U9QF3C6X1A`;
3762
+ }
3763
+ const prefix = normalizedUserId.charAt(0);
3764
+ if (!"UBPT".includes(prefix)) {
3765
+ return `User ID must start with U (user), B (bot), P (project), or T (team). Got: "${prefix}". Check your DeskFree account settings.`;
3766
+ }
3767
+ if (!/^[A-Z0-9]+$/.test(normalizedUserId.slice(1))) {
3768
+ return "User ID contains invalid characters. Only letters A-Z and numbers 0-9 are allowed after the prefix.";
3769
+ }
3770
+ return "User ID must be a valid DeskFree ID format: one letter (U/B/P/T) + 10 alphanumeric characters (e.g. U9QF3C6X1A)";
3771
+ }
3772
+ if (["U0000000000", "B0000000000", "P0000000000", "T0000000000"].includes(
3773
+ normalizedUserId
3774
+ )) {
3775
+ return "Please replace the placeholder with your actual User ID from DeskFree account settings";
3776
+ }
3777
+ return null;
3778
+ }
3779
+
3654
3780
  // src/client.ts
3655
3781
  var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
3656
3782
  var DeskFreeError = class _DeskFreeError extends Error {
@@ -4117,6 +4243,92 @@ function resolvePluginStorePath(subpath) {
4117
4243
  return join(stateDir, "plugin-data", "deskfree", subpath);
4118
4244
  }
4119
4245
 
4246
+ // src/gateway.cursor.ts
4247
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
4248
+ import { dirname } from "path";
4249
+ function loadCursor(ctx) {
4250
+ try {
4251
+ const cursorPath = resolvePluginStorePath(
4252
+ `cursors/${ctx.accountId}/cursor`
4253
+ );
4254
+ return readFileSync(cursorPath, "utf-8").trim() || null;
4255
+ } catch {
4256
+ return null;
4257
+ }
4258
+ }
4259
+ function saveCursor(ctx, cursor, log) {
4260
+ try {
4261
+ const filePath = resolvePluginStorePath(`cursors/${ctx.accountId}/cursor`);
4262
+ const dir = dirname(filePath);
4263
+ mkdirSync(dir, { recursive: true });
4264
+ writeFileSync(filePath, cursor, "utf-8");
4265
+ } catch (err) {
4266
+ const message = err instanceof Error ? err.message : String(err);
4267
+ log?.warn(`Failed to persist cursor: ${message}`);
4268
+ }
4269
+ }
4270
+
4271
+ // src/gateway.health.ts
4272
+ var accountHealth = /* @__PURE__ */ new Map();
4273
+ function initializeHealth(accountId) {
4274
+ if (!accountHealth.has(accountId)) {
4275
+ accountHealth.set(accountId, {
4276
+ connectionStartTime: Date.now(),
4277
+ totalReconnects: 0,
4278
+ lastReconnectAt: null,
4279
+ avgReconnectInterval: 0,
4280
+ totalMessagesDelivered: 0,
4281
+ lastMessageAt: null,
4282
+ currentMode: "websocket"
4283
+ });
4284
+ }
4285
+ }
4286
+ function updateHealthMode(accountId, mode) {
4287
+ const health = accountHealth.get(accountId);
4288
+ if (health) {
4289
+ health.currentMode = mode;
4290
+ }
4291
+ }
4292
+ function recordReconnect(accountId) {
4293
+ const health = accountHealth.get(accountId);
4294
+ if (health) {
4295
+ const now = Date.now();
4296
+ if (health.lastReconnectAt) {
4297
+ const interval = now - health.lastReconnectAt;
4298
+ health.avgReconnectInterval = (health.avgReconnectInterval * health.totalReconnects + interval) / (health.totalReconnects + 1);
4299
+ }
4300
+ health.totalReconnects++;
4301
+ health.lastReconnectAt = now;
4302
+ }
4303
+ }
4304
+ function recordMessageDelivery(accountId, count) {
4305
+ const health = accountHealth.get(accountId);
4306
+ if (health) {
4307
+ health.totalMessagesDelivered += count;
4308
+ health.lastMessageAt = Date.now();
4309
+ }
4310
+ }
4311
+ function formatDuration(ms) {
4312
+ const seconds = Math.floor(ms / 1e3);
4313
+ const minutes = Math.floor(seconds / 60);
4314
+ const hours = Math.floor(minutes / 60);
4315
+ if (hours > 0) {
4316
+ return `${hours}h${minutes % 60}m`;
4317
+ } else if (minutes > 0) {
4318
+ return `${minutes}m${seconds % 60}s`;
4319
+ } else {
4320
+ return `${seconds}s`;
4321
+ }
4322
+ }
4323
+ function logHealthSummary(accountId, log) {
4324
+ const health = accountHealth.get(accountId);
4325
+ if (!health) return;
4326
+ const uptime = Date.now() - health.connectionStartTime;
4327
+ log.info(
4328
+ `DeskFree health: uptime=${formatDuration(uptime)}, reconnects=${health.totalReconnects}, messages=${health.totalMessagesDelivered}, mode=${health.currentMode}`
4329
+ );
4330
+ }
4331
+
4120
4332
  // src/streaming.ts
4121
4333
  var THROTTLE_MS = 300;
4122
4334
  var CHAR_BUFFER_SIZE = 256;
@@ -4258,7 +4470,7 @@ var DeskFreeStreamingSession = class {
4258
4470
  import { randomUUID } from "crypto";
4259
4471
  import { createWriteStream } from "fs";
4260
4472
  import { mkdir, unlink } from "fs/promises";
4261
- import { dirname, extname } from "path";
4473
+ import { dirname as dirname2, extname } from "path";
4262
4474
  import { Readable } from "stream";
4263
4475
  import { pipeline } from "stream/promises";
4264
4476
  var MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
@@ -4381,7 +4593,7 @@ async function fetchAndSaveMedia(attachment) {
4381
4593
  throw new Error("Generated filename too long (max 255 characters)");
4382
4594
  }
4383
4595
  const filePath = resolvePluginStorePath(`media/inbound/${fileName}`);
4384
- await mkdir(dirname(filePath), { recursive: true });
4596
+ await mkdir(dirname2(filePath), { recursive: true });
4385
4597
  const controller = new AbortController();
4386
4598
  const timeoutId = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
4387
4599
  let fileStream = null;
@@ -4699,17 +4911,6 @@ async function deliverMessageToAgent(ctx, message, client) {
4699
4911
  }
4700
4912
  }
4701
4913
 
4702
- // src/version.ts
4703
- import { readFileSync } from "fs";
4704
- import { resolve } from "path";
4705
- var PLUGIN_VERSION = JSON.parse(
4706
- readFileSync(resolve(__dirname, "..", "package.json"), "utf-8")
4707
- ).version;
4708
-
4709
- // src/gateway.ts
4710
- import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
4711
- import { dirname as dirname2 } from "path";
4712
-
4713
4914
  // ../node_modules/ws/wrapper.mjs
4714
4915
  var import_stream = __toESM(require_stream(), 1);
4715
4916
  var import_receiver = __toESM(require_receiver(), 1);
@@ -4718,8 +4919,11 @@ var import_websocket = __toESM(require_websocket(), 1);
4718
4919
  var import_websocket_server = __toESM(require_websocket_server(), 1);
4719
4920
  var wrapper_default = import_websocket.default;
4720
4921
 
4721
- // src/gateway.ts
4922
+ // src/gateway.state.ts
4722
4923
  var activeWs = null;
4924
+ function setActiveWs(ws) {
4925
+ activeWs = ws;
4926
+ }
4723
4927
  function sendWsAck(messageId) {
4724
4928
  if (activeWs?.readyState === wrapper_default.OPEN) {
4725
4929
  try {
@@ -4756,17 +4960,6 @@ function clearCompletedTaskId() {
4756
4960
  function setInboundThreadId(taskId) {
4757
4961
  inboundThreadId = taskId;
4758
4962
  }
4759
- var PING_INTERVAL_MS = 5 * 60 * 1e3;
4760
- var POLL_FALLBACK_INTERVAL_MS = 30 * 1e3;
4761
- var WS_CONNECTION_TIMEOUT_MS = 30 * 1e3;
4762
- var WS_PONG_TIMEOUT_MS = 10 * 1e3;
4763
- var NOTIFY_DEBOUNCE_MS = 200;
4764
- var BACKOFF_INITIAL_MS = 2e3;
4765
- var BACKOFF_MAX_MS = 3e4;
4766
- var BACKOFF_FACTOR = 1.8;
4767
- var HEALTH_LOG_INTERVAL_MS = 30 * 60 * 1e3;
4768
- var MAX_CONSECUTIVE_POLL_FAILURES = 5;
4769
- var deliveredMessageIds = /* @__PURE__ */ new Set();
4770
4963
  function sendWsStreamChunk(messageId, delta, fullContent) {
4771
4964
  if (!activeWs || activeWs.readyState !== wrapper_default.OPEN) return false;
4772
4965
  try {
@@ -4778,67 +4971,11 @@ function sendWsStreamChunk(messageId, delta, fullContent) {
4778
4971
  return false;
4779
4972
  }
4780
4973
  }
4781
- var healthState = /* @__PURE__ */ new Map();
4782
- function initializeHealth(accountId) {
4783
- if (!healthState.has(accountId)) {
4784
- healthState.set(accountId, {
4785
- connectionStartTime: Date.now(),
4786
- totalReconnects: 0,
4787
- lastReconnectAt: null,
4788
- avgReconnectInterval: 0,
4789
- totalMessagesDelivered: 0,
4790
- lastMessageAt: null,
4791
- currentMode: "websocket"
4792
- });
4793
- }
4794
- }
4795
- function updateHealthMode(accountId, mode) {
4796
- const health = healthState.get(accountId);
4797
- if (health) {
4798
- health.currentMode = mode;
4799
- }
4800
- }
4801
- function recordReconnect(accountId) {
4802
- const health = healthState.get(accountId);
4803
- if (health) {
4804
- const now = Date.now();
4805
- if (health.lastReconnectAt) {
4806
- const interval = now - health.lastReconnectAt;
4807
- health.avgReconnectInterval = (health.avgReconnectInterval * health.totalReconnects + interval) / (health.totalReconnects + 1);
4808
- }
4809
- health.totalReconnects++;
4810
- health.lastReconnectAt = now;
4811
- }
4812
- }
4813
- function recordMessageDelivery(accountId, count) {
4814
- const health = healthState.get(accountId);
4815
- if (health) {
4816
- health.totalMessagesDelivered += count;
4817
- health.lastMessageAt = Date.now();
4818
- }
4819
- }
4820
- function formatDuration(ms) {
4821
- const seconds = Math.floor(ms / 1e3);
4822
- const minutes = Math.floor(seconds / 60);
4823
- const hours = Math.floor(minutes / 60);
4824
- if (hours > 0) {
4825
- return `${hours}h${minutes % 60}m`;
4826
- } else if (minutes > 0) {
4827
- return `${minutes}m${seconds % 60}s`;
4828
- } else {
4829
- return `${seconds}s`;
4830
- }
4831
- }
4832
- function logHealthSummary(accountId, log) {
4833
- const health = healthState.get(accountId);
4834
- if (!health) return;
4835
- const uptime = Date.now() - health.connectionStartTime;
4836
- log.info(
4837
- `DeskFree health: uptime=${formatDuration(uptime)}, reconnects=${health.totalReconnects}, messages=${health.totalMessagesDelivered}, mode=${health.currentMode}`
4838
- );
4839
- }
4974
+
4975
+ // src/gateway.polling.ts
4976
+ var deliveredMessageIds = /* @__PURE__ */ new Set();
4840
4977
  var pollChains = /* @__PURE__ */ new Map();
4841
- function enqueuePoll(client, ctx, getCursor, setCursor, log, account) {
4978
+ function enqueuePoll(client, ctx, getCursor, setCursor, log, account, getWelcomeSent, setWelcomeSent) {
4842
4979
  const accountId = ctx.accountId;
4843
4980
  const prev = pollChains.get(accountId) ?? Promise.resolve();
4844
4981
  const next = prev.then(async () => {
@@ -4847,15 +4984,137 @@ function enqueuePoll(client, ctx, getCursor, setCursor, log, account) {
4847
4984
  ctx,
4848
4985
  getCursor(),
4849
4986
  log,
4850
- account
4987
+ account,
4988
+ getWelcomeSent?.() ?? false
4851
4989
  );
4852
4990
  if (result.cursor) setCursor(result.cursor);
4991
+ if (result.welcomeSent) setWelcomeSent?.(true);
4853
4992
  }).catch((err) => {
4854
4993
  const message = err instanceof Error ? err.message : String(err);
4855
4994
  log.error(`Poll error: ${message}`);
4856
4995
  });
4857
4996
  pollChains.set(accountId, next);
4858
4997
  }
4998
+ var SEED = "SEED";
4999
+ async function pollAndDeliver(client, ctx, cursor, log, account, alreadyWelcomed) {
5000
+ try {
5001
+ const isFirstRun = !cursor || cursor === SEED;
5002
+ const apiCursor = cursor && cursor !== SEED ? cursor : void 0;
5003
+ const response = await client.listMessages({
5004
+ ...apiCursor ? { cursor: apiCursor } : {}
5005
+ });
5006
+ if (isFirstRun) {
5007
+ if (response.cursor) {
5008
+ log.info(
5009
+ `First run: skipping ${response.items.length} existing message(s), seeding cursor.`
5010
+ );
5011
+ saveCursor(ctx, response.cursor, log);
5012
+ } else {
5013
+ log.info("First run: no messages yet (empty inbox).");
5014
+ }
5015
+ log.info("Connected to DeskFree. Ready to receive messages and tasks.");
5016
+ if (alreadyWelcomed) {
5017
+ log.info("Welcome already sent, skipping duplicate.");
5018
+ return {
5019
+ cursor: response.cursor ?? SEED,
5020
+ ok: true,
5021
+ welcomeSent: false
5022
+ };
5023
+ }
5024
+ try {
5025
+ const botName = account?.botName;
5026
+ const humanName = account?.humanName;
5027
+ const welcomeContent = botName && humanName ? `Hey ${botName}! I'm ${humanName}. We're connected through DeskFree \u2014 I'll send you tasks and you'll help me get things done. What should we work on first? (Just reply to this \u2014 don't introduce yourself again on heartbeat, wait for my response.)` : "DeskFree is connected! Send me tasks and I'll help you get things done. What should we work on first? (Just reply to this \u2014 don't introduce yourself again on heartbeat, wait for my response.)";
5028
+ const welcomeMessage = {
5029
+ messageId: `welcome-${Date.now()}`,
5030
+ botId: "",
5031
+ humanId: "system",
5032
+ authorType: "user",
5033
+ content: welcomeContent,
5034
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5035
+ userName: humanName ?? "System"
5036
+ };
5037
+ await deliverMessageToAgent(ctx, welcomeMessage, client);
5038
+ log.info("Sent welcome message to agent.");
5039
+ } catch (err) {
5040
+ const msg = err instanceof Error ? err.message : String(err);
5041
+ log.warn(`Failed to send welcome message: ${msg}`);
5042
+ }
5043
+ return { cursor: response.cursor ?? SEED, ok: true, welcomeSent: true };
5044
+ }
5045
+ if (response.items.length === 0) return { cursor: null, ok: true };
5046
+ const newItems = response.items.filter(
5047
+ (m) => !deliveredMessageIds.has(m.messageId)
5048
+ );
5049
+ if (newItems.length === 0) {
5050
+ log.debug(
5051
+ `Poll returned ${response.items.length} item(s), all already delivered.`
5052
+ );
5053
+ if (response.cursor) {
5054
+ saveCursor(ctx, response.cursor, log);
5055
+ }
5056
+ return { cursor: response.cursor, ok: true };
5057
+ }
5058
+ log.info(
5059
+ `Received ${newItems.length} new message(s) (${response.items.length - newItems.length} duplicate(s) skipped).`
5060
+ );
5061
+ let deliveredCount = 0;
5062
+ for (const message of newItems) {
5063
+ if (message.authorType === "bot") {
5064
+ log.debug(`Skipping bot message ${message.messageId}`);
5065
+ deliveredMessageIds.add(message.messageId);
5066
+ continue;
5067
+ }
5068
+ clearCompletedTaskId();
5069
+ setInboundThreadId(message.taskId ?? null);
5070
+ await deliverMessageToAgent(ctx, message, client);
5071
+ deliveredMessageIds.add(message.messageId);
5072
+ deliveredCount++;
5073
+ sendWsAck(message.messageId);
5074
+ }
5075
+ if (deliveredMessageIds.size > 1e3) {
5076
+ const entries = Array.from(deliveredMessageIds);
5077
+ deliveredMessageIds.clear();
5078
+ for (const id of entries.slice(-500)) {
5079
+ deliveredMessageIds.add(id);
5080
+ }
5081
+ }
5082
+ if (deliveredCount > 0) {
5083
+ recordMessageDelivery(ctx.accountId, deliveredCount);
5084
+ }
5085
+ if (response.cursor) {
5086
+ saveCursor(ctx, response.cursor, log);
5087
+ }
5088
+ return { cursor: response.cursor, ok: true };
5089
+ } catch (err) {
5090
+ const message = err instanceof Error ? err.message : String(err);
5091
+ log.warn(`Poll failed: ${message}`);
5092
+ reportError("warn", `Message poll failed: ${message}`, {
5093
+ component: "gateway",
5094
+ event: "poll_failed"
5095
+ });
5096
+ return { cursor: null, ok: false };
5097
+ }
5098
+ }
5099
+
5100
+ // src/version.ts
5101
+ import { readFileSync as readFileSync2 } from "fs";
5102
+ import { resolve } from "path";
5103
+ var PLUGIN_VERSION = JSON.parse(
5104
+ readFileSync2(resolve(__dirname, "..", "package.json"), "utf-8")
5105
+ ).version;
5106
+
5107
+ // src/gateway.ts
5108
+ var PING_INTERVAL_MS = 5 * 60 * 1e3;
5109
+ var POLL_FALLBACK_INTERVAL_MS = 30 * 1e3;
5110
+ var WS_CONNECTION_TIMEOUT_MS = 30 * 1e3;
5111
+ var WS_PONG_TIMEOUT_MS = 10 * 1e3;
5112
+ var NOTIFY_DEBOUNCE_MS = 200;
5113
+ var BACKOFF_INITIAL_MS = 2e3;
5114
+ var BACKOFF_MAX_MS = 3e4;
5115
+ var BACKOFF_FACTOR = 1.8;
5116
+ var HEALTH_LOG_INTERVAL_MS = 30 * 60 * 1e3;
5117
+ var MAX_CONSECUTIVE_POLL_FAILURES = 5;
4859
5118
  function nextBackoff(state) {
4860
5119
  const delay = Math.min(
4861
5120
  BACKOFF_INITIAL_MS * Math.pow(BACKOFF_FACTOR, state.attempt),
@@ -4881,32 +5140,12 @@ function sleepWithAbort(ms, signal) {
4881
5140
  signal.addEventListener("abort", onAbort, { once: true });
4882
5141
  });
4883
5142
  }
4884
- function loadCursor(ctx) {
4885
- try {
4886
- const cursorPath = resolvePluginStorePath(
4887
- `cursors/${ctx.accountId}/cursor`
4888
- );
4889
- return readFileSync2(cursorPath, "utf-8").trim() || null;
4890
- } catch {
4891
- return null;
4892
- }
4893
- }
4894
- function saveCursor(ctx, cursor, log) {
4895
- try {
4896
- const filePath = resolvePluginStorePath(`cursors/${ctx.accountId}/cursor`);
4897
- const dir = dirname2(filePath);
4898
- mkdirSync(dir, { recursive: true });
4899
- writeFileSync(filePath, cursor, "utf-8");
4900
- } catch (err) {
4901
- const message = err instanceof Error ? err.message : String(err);
4902
- log?.warn(`Failed to persist cursor: ${message}`);
4903
- }
4904
- }
4905
5143
  async function startDeskFreeConnection(ctx) {
4906
5144
  const account = ctx.account;
4907
5145
  const client = new DeskFreeClient(account.botToken, account.apiUrl);
4908
5146
  const log = ctx.log ?? getDeskFreeRuntime().logging.createLogger("deskfree");
4909
5147
  let cursor = loadCursor(ctx);
5148
+ let welcomeSent = false;
4910
5149
  const backoff = { attempt: 0 };
4911
5150
  let totalReconnects = 0;
4912
5151
  initializeHealth(ctx.accountId);
@@ -4946,7 +5185,11 @@ async function startDeskFreeConnection(ctx) {
4946
5185
  ctx,
4947
5186
  cursor,
4948
5187
  log,
4949
- account
5188
+ account,
5189
+ getWelcomeSent: () => welcomeSent,
5190
+ setWelcomeSent: (v) => {
5191
+ welcomeSent = v;
5192
+ }
4950
5193
  });
4951
5194
  totalReconnects++;
4952
5195
  } catch (err) {
@@ -4988,7 +5231,16 @@ async function startDeskFreeConnection(ctx) {
4988
5231
  );
4989
5232
  }
4990
5233
  async function runWebSocketConnection(opts) {
4991
- const { ticket, wsUrl, client, ctx, log, account } = opts;
5234
+ const {
5235
+ ticket,
5236
+ wsUrl,
5237
+ client,
5238
+ ctx,
5239
+ log,
5240
+ account,
5241
+ getWelcomeSent,
5242
+ setWelcomeSent
5243
+ } = opts;
4992
5244
  let cursor = opts.cursor;
4993
5245
  return new Promise((resolve2, reject) => {
4994
5246
  const ws = new wrapper_default(`${wsUrl}?ticket=${ticket}`);
@@ -5031,7 +5283,7 @@ async function runWebSocketConnection(opts) {
5031
5283
  }, WS_CONNECTION_TIMEOUT_MS);
5032
5284
  ws.on("open", async () => {
5033
5285
  isConnected = true;
5034
- activeWs = ws;
5286
+ setActiveWs(ws);
5035
5287
  if (connectionTimer !== void 0) {
5036
5288
  clearTimeout(connectionTimer);
5037
5289
  connectionTimer = void 0;
@@ -5077,7 +5329,9 @@ async function runWebSocketConnection(opts) {
5077
5329
  cursor = c ?? cursor;
5078
5330
  },
5079
5331
  log,
5080
- account
5332
+ account,
5333
+ getWelcomeSent,
5334
+ setWelcomeSent
5081
5335
  );
5082
5336
  client.listMessages({ limit: 20 }).then((res) => {
5083
5337
  const humanMessages = res.items?.filter(
@@ -5117,7 +5371,10 @@ async function runWebSocketConnection(opts) {
5117
5371
  (c) => {
5118
5372
  cursor = c ?? cursor;
5119
5373
  },
5120
- log
5374
+ log,
5375
+ account,
5376
+ getWelcomeSent,
5377
+ setWelcomeSent
5121
5378
  );
5122
5379
  }, NOTIFY_DEBOUNCE_MS);
5123
5380
  } else if (msg.action === "pong") {
@@ -5135,7 +5392,7 @@ async function runWebSocketConnection(opts) {
5135
5392
  ws.on("close", (code, reason) => {
5136
5393
  cleanup();
5137
5394
  isConnected = false;
5138
- if (activeWs === ws) activeWs = null;
5395
+ if (activeWs === ws) setActiveWs(null);
5139
5396
  ctx.setStatus({ running: false, lastStopAt: Date.now() });
5140
5397
  if (code === 1e3) {
5141
5398
  log.info(`WebSocket closed normally: ${code} ${reason.toString()}`);
@@ -5185,7 +5442,7 @@ async function runWebSocketConnection(opts) {
5185
5442
  ws.on("error", (err) => {
5186
5443
  cleanup();
5187
5444
  isConnected = false;
5188
- if (activeWs === ws) activeWs = null;
5445
+ if (activeWs === ws) setActiveWs(null);
5189
5446
  const errorMessage = err instanceof Error ? err.message : String(err);
5190
5447
  log.error(`WebSocket error: ${errorMessage}`);
5191
5448
  reportError("error", `WebSocket error: ${errorMessage}`, {
@@ -5209,144 +5466,51 @@ async function runWebSocketConnection(opts) {
5209
5466
  ws.close(1e3, "shutdown");
5210
5467
  }
5211
5468
  } catch (err) {
5212
- const message = err instanceof Error ? err.message : String(err);
5213
- log.warn(`Error closing WebSocket during shutdown: ${message}`);
5214
- }
5215
- },
5216
- { once: true }
5217
- );
5218
- });
5219
- }
5220
- async function runPollingFallback(opts) {
5221
- const { client, ctx, log } = opts;
5222
- let cursor = opts.cursor;
5223
- let iterations = 0;
5224
- const maxIterations = 10;
5225
- ctx.setStatus({ running: true, lastStartAt: Date.now() });
5226
- log.info("Running in polling fallback mode.");
5227
- let consecutiveFailures = 0;
5228
- while (!ctx.abortSignal.aborted && iterations < maxIterations) {
5229
- const result = await pollAndDeliver(client, ctx, cursor, log);
5230
- if (result.ok) {
5231
- if (result.cursor) cursor = result.cursor;
5232
- consecutiveFailures = 0;
5233
- } else {
5234
- consecutiveFailures++;
5235
- if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
5236
- log.warn(
5237
- `${consecutiveFailures} consecutive poll failures, breaking out to retry WebSocket`
5238
- );
5239
- reportError(
5240
- "error",
5241
- `${consecutiveFailures} consecutive poll failures, switching to WebSocket retry`,
5242
- {
5243
- component: "gateway",
5244
- event: "poll_max_failures",
5245
- consecutiveFailures
5246
- }
5247
- );
5248
- break;
5249
- }
5250
- }
5251
- iterations++;
5252
- const jitter = Math.random() * POLL_FALLBACK_INTERVAL_MS * 0.2;
5253
- await sleepWithAbort(POLL_FALLBACK_INTERVAL_MS + jitter, ctx.abortSignal);
5254
- }
5255
- ctx.setStatus({ running: false, lastStopAt: Date.now() });
5256
- return cursor;
5257
- }
5258
- async function pollAndDeliver(client, ctx, cursor, log, account) {
5259
- try {
5260
- const SEED = "SEED";
5261
- const isFirstRun = !cursor || cursor === SEED;
5262
- const apiCursor = cursor && cursor !== SEED ? cursor : void 0;
5263
- const response = await client.listMessages({
5264
- ...apiCursor ? { cursor: apiCursor } : {}
5265
- });
5266
- if (isFirstRun) {
5267
- if (response.cursor) {
5268
- log.info(
5269
- `First run: skipping ${response.items.length} existing message(s), seeding cursor.`
5270
- );
5271
- saveCursor(ctx, response.cursor, log);
5272
- } else {
5273
- log.info("First run: no messages yet (empty inbox).");
5274
- }
5275
- log.info("Connected to DeskFree. Ready to receive messages and tasks.");
5276
- try {
5277
- const botName = account?.botName;
5278
- const humanName = account?.humanName;
5279
- const welcomeContent = botName && humanName ? `Hey ${botName}! I'm ${humanName}. We're connected through DeskFree \u2014 I'll send you tasks and you'll help me get things done. What should we work on first? (Just reply to this \u2014 don't introduce yourself again on heartbeat, wait for my response.)` : "DeskFree is connected! Send me tasks and I'll help you get things done. What should we work on first? (Just reply to this \u2014 don't introduce yourself again on heartbeat, wait for my response.)";
5280
- const welcomeMessage = {
5281
- messageId: `welcome-${Date.now()}`,
5282
- botId: "",
5283
- humanId: "system",
5284
- authorType: "user",
5285
- content: welcomeContent,
5286
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5287
- userName: humanName ?? "System"
5288
- };
5289
- await deliverMessageToAgent(ctx, welcomeMessage, client);
5290
- log.info("Sent welcome message to agent.");
5291
- } catch (err) {
5292
- const msg = err instanceof Error ? err.message : String(err);
5293
- log.warn(`Failed to send welcome message: ${msg}`);
5294
- }
5295
- return { cursor: response.cursor ?? SEED, ok: true };
5296
- }
5297
- if (response.items.length === 0) return { cursor: null, ok: true };
5298
- const newItems = response.items.filter(
5299
- (m) => !deliveredMessageIds.has(m.messageId)
5300
- );
5301
- if (newItems.length === 0) {
5302
- log.debug(
5303
- `Poll returned ${response.items.length} item(s), all already delivered.`
5304
- );
5305
- if (response.cursor) {
5306
- saveCursor(ctx, response.cursor, log);
5307
- }
5308
- return { cursor: response.cursor, ok: true };
5309
- }
5310
- log.info(
5311
- `Received ${newItems.length} new message(s) (${response.items.length - newItems.length} duplicate(s) skipped).`
5469
+ const message = err instanceof Error ? err.message : String(err);
5470
+ log.warn(`Error closing WebSocket during shutdown: ${message}`);
5471
+ }
5472
+ },
5473
+ { once: true }
5312
5474
  );
5313
- let deliveredCount = 0;
5314
- for (const message of newItems) {
5315
- if (message.authorType === "bot") {
5316
- log.debug(`Skipping bot message ${message.messageId}`);
5317
- deliveredMessageIds.add(message.messageId);
5318
- continue;
5319
- }
5320
- clearCompletedTaskId();
5321
- setInboundThreadId(message.taskId ?? null);
5322
- await deliverMessageToAgent(ctx, message, client);
5323
- deliveredMessageIds.add(message.messageId);
5324
- deliveredCount++;
5325
- sendWsAck(message.messageId);
5326
- }
5327
- if (deliveredMessageIds.size > 1e3) {
5328
- const entries = Array.from(deliveredMessageIds);
5329
- deliveredMessageIds.clear();
5330
- for (const id of entries.slice(-500)) {
5331
- deliveredMessageIds.add(id);
5475
+ });
5476
+ }
5477
+ async function runPollingFallback(opts) {
5478
+ const { client, ctx, log } = opts;
5479
+ let cursor = opts.cursor;
5480
+ let iterations = 0;
5481
+ const maxIterations = 10;
5482
+ ctx.setStatus({ running: true, lastStartAt: Date.now() });
5483
+ log.info("Running in polling fallback mode.");
5484
+ let consecutiveFailures = 0;
5485
+ while (!ctx.abortSignal.aborted && iterations < maxIterations) {
5486
+ const result = await pollAndDeliver(client, ctx, cursor, log);
5487
+ if (result.ok) {
5488
+ if (result.cursor) cursor = result.cursor;
5489
+ consecutiveFailures = 0;
5490
+ } else {
5491
+ consecutiveFailures++;
5492
+ if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
5493
+ log.warn(
5494
+ `${consecutiveFailures} consecutive poll failures, breaking out to retry WebSocket`
5495
+ );
5496
+ reportError(
5497
+ "error",
5498
+ `${consecutiveFailures} consecutive poll failures, switching to WebSocket retry`,
5499
+ {
5500
+ component: "gateway",
5501
+ event: "poll_max_failures",
5502
+ consecutiveFailures
5503
+ }
5504
+ );
5505
+ break;
5332
5506
  }
5333
5507
  }
5334
- if (deliveredCount > 0) {
5335
- recordMessageDelivery(ctx.accountId, deliveredCount);
5336
- }
5337
- if (response.cursor) {
5338
- saveCursor(ctx, response.cursor, log);
5339
- }
5340
- return { cursor: response.cursor, ok: true };
5341
- } catch (err) {
5342
- const message = err instanceof Error ? err.message : String(err);
5343
- log.warn(`Poll failed: ${message}`);
5344
- reportError("warn", `Message poll failed: ${message}`, {
5345
- component: "gateway",
5346
- event: "poll_failed"
5347
- });
5348
- return { cursor: null, ok: false };
5508
+ iterations++;
5509
+ const jitter = Math.random() * POLL_FALLBACK_INTERVAL_MS * 0.2;
5510
+ await sleepWithAbort(POLL_FALLBACK_INTERVAL_MS + jitter, ctx.abortSignal);
5349
5511
  }
5512
+ ctx.setStatus({ running: false, lastStopAt: Date.now() });
5513
+ return cursor;
5350
5514
  }
5351
5515
 
5352
5516
  // node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
@@ -8349,131 +8513,6 @@ var STATUS_MESSAGES = {
8349
8513
  function getChannelConfig(cfg) {
8350
8514
  return cfg?.channels?.deskfree ?? null;
8351
8515
  }
8352
- function validateStringField(value, fieldName) {
8353
- if (!value) {
8354
- return { trimmed: "", error: `${fieldName} is required` };
8355
- }
8356
- if (typeof value !== "string") {
8357
- return { trimmed: "", error: `${fieldName} must be a string` };
8358
- }
8359
- const trimmed = value.trim();
8360
- if (trimmed !== value) {
8361
- return {
8362
- trimmed: "",
8363
- error: `${fieldName} must not have leading or trailing whitespace`
8364
- };
8365
- }
8366
- return { trimmed };
8367
- }
8368
- function isLocalDevelopmentHost(hostname) {
8369
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.endsWith(".local") || hostname.endsWith(".localhost") || /^192\.168\./.test(hostname) || /^10\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname);
8370
- }
8371
- function validateBotToken(value) {
8372
- const { trimmed, error } = validateStringField(value, "Bot token");
8373
- if (error) return error;
8374
- if (!trimmed.startsWith("bot_")) {
8375
- return 'Bot token must start with "bot_" (check your DeskFree bot configuration)';
8376
- }
8377
- if (trimmed.length < 10) {
8378
- return "Bot token appears to be incomplete (minimum 10 characters expected)";
8379
- }
8380
- if (trimmed.length > 200) {
8381
- return "Bot token appears to be invalid (maximum 200 characters expected)";
8382
- }
8383
- if (!/^bot_[a-zA-Z0-9_-]+$/.test(trimmed)) {
8384
- return 'Bot token contains invalid characters. Only alphanumeric, underscore, and dash are allowed after "bot_"';
8385
- }
8386
- if (trimmed.includes(" ") || trimmed.includes("\n") || trimmed.includes(" ")) {
8387
- return "Bot token contains whitespace characters. Please copy the token exactly as shown in DeskFree.";
8388
- }
8389
- if (trimmed === "bot_your_token_here" || trimmed === "bot_example") {
8390
- return "Please replace the placeholder with your actual bot token from DeskFree";
8391
- }
8392
- return null;
8393
- }
8394
- function validateApiUrl(value) {
8395
- const { trimmed, error } = validateStringField(value, "API URL");
8396
- if (error) return error;
8397
- let url;
8398
- try {
8399
- url = new URL(trimmed);
8400
- } catch (err) {
8401
- const message = err instanceof Error ? err.message : "Invalid URL format";
8402
- return `API URL must be a valid URL: ${message}`;
8403
- }
8404
- if (url.protocol !== "https:") {
8405
- return "API URL must use HTTPS protocol for security. Make sure your DeskFree deployment supports HTTPS.";
8406
- }
8407
- if (!url.hostname) {
8408
- return "API URL must have a valid hostname";
8409
- }
8410
- if (isLocalDevelopmentHost(url.hostname)) {
8411
- if (process.env.NODE_ENV === "production") {
8412
- return "API URL cannot use localhost or private IP addresses in production. Please use a publicly accessible URL.";
8413
- }
8414
- }
8415
- if (url.hostname.includes("..") || url.hostname.startsWith(".")) {
8416
- return "API URL hostname appears to be malformed. Please check for typos.";
8417
- }
8418
- if (url.pathname && url.pathname !== "/" && !url.pathname.includes("/api") && !url.pathname.includes("/v1")) {
8419
- }
8420
- return null;
8421
- }
8422
- function validateWebSocketUrl(value) {
8423
- const { trimmed, error } = validateStringField(value, "WebSocket URL");
8424
- if (error) return error;
8425
- let url;
8426
- try {
8427
- url = new URL(trimmed);
8428
- } catch (err) {
8429
- const message = err instanceof Error ? err.message : "Invalid URL format";
8430
- return `WebSocket URL must be a valid URL: ${message}`;
8431
- }
8432
- if (!["ws:", "wss:"].includes(url.protocol)) {
8433
- return "WebSocket URL must use ws:// or wss:// protocol";
8434
- }
8435
- if (!url.hostname) {
8436
- return "WebSocket URL must have a valid hostname";
8437
- }
8438
- if (isLocalDevelopmentHost(url.hostname)) {
8439
- if (process.env.NODE_ENV === "production") {
8440
- return "WebSocket URL cannot use localhost or private IP addresses in production. Please use a publicly accessible URL.";
8441
- }
8442
- }
8443
- if (url.protocol === "ws:" && !isLocalDevelopmentHost(url.hostname)) {
8444
- }
8445
- if (url.hostname.includes("..") || url.hostname.startsWith(".")) {
8446
- return "WebSocket URL hostname appears to be malformed. Please check for typos.";
8447
- }
8448
- return null;
8449
- }
8450
- function validateUserId(value) {
8451
- const { trimmed, error } = validateStringField(value, "User ID");
8452
- if (error) return error;
8453
- const normalizedUserId = trimmed.toUpperCase();
8454
- if (normalizedUserId !== trimmed) {
8455
- return "User ID should be uppercase (will be automatically converted)";
8456
- }
8457
- if (!/^[UBPT][A-Z0-9]{10}$/.test(normalizedUserId)) {
8458
- if (normalizedUserId.length !== 11) {
8459
- return `User ID must be exactly 11 characters long (got: ${normalizedUserId.length}). Example: U9QF3C6X1A`;
8460
- }
8461
- const prefix = normalizedUserId.charAt(0);
8462
- if (!"UBPT".includes(prefix)) {
8463
- return `User ID must start with U (user), B (bot), P (project), or T (team). Got: "${prefix}". Check your DeskFree account settings.`;
8464
- }
8465
- if (!/^[A-Z0-9]+$/.test(normalizedUserId.slice(1))) {
8466
- return "User ID contains invalid characters. Only letters A-Z and numbers 0-9 are allowed after the prefix.";
8467
- }
8468
- return "User ID must be a valid DeskFree ID format: one letter (U/B/P/T) + 10 alphanumeric characters (e.g. U9QF3C6X1A)";
8469
- }
8470
- if (["U0000000000", "B0000000000", "P0000000000", "T0000000000"].includes(
8471
- normalizedUserId
8472
- )) {
8473
- return "Please replace the placeholder with your actual User ID from DeskFree account settings";
8474
- }
8475
- return null;
8476
- }
8477
8516
  var deskFreePlugin = {
8478
8517
  id: "deskfree",
8479
8518
  meta: CHANNEL_META,
@@ -8894,8 +8933,8 @@ You are the orchestrator. Your job: turn human intent into approved tasks, then
8894
8933
 
8895
8934
  **Match the human's energy.** Short message \u2192 short reply. Casual tone \u2192 casual response. Don't over-explain, don't lecture, don't pad responses.
8896
8935
 
8897
- You do NOT claim tasks or do work directly \u2014 you have no access to deskfree_start_task. Spawn a sub-agent for each approved task and pass it the taskId.
8898
- - When a human writes in a task thread, decide: does it need bot action? If yes \u2192 reopen and spawn sub-agent. If it's confirmation ("looks good") \u2192 complete the task. If deferred ("I'll check later") \u2192 leave it.
8936
+ You do NOT claim tasks, complete tasks, or do work directly \u2014 you have no access to deskfree_start_task or deskfree_complete_task. Spawn a sub-agent for each approved task and pass it the taskId.
8937
+ - When a human writes in a task thread, decide: does it need bot action? If yes \u2192 reopen and spawn sub-agent. If it's just confirmation or deferred \u2014 leave it for now.
8899
8938
  - Write task instructions as rich markdown (bold, lists, inline code \u2014 no # headers). Brief a contractor who has never seen the codebase.
8900
8939
  - Estimate token cost per task \u2014 consider files to read, reasoning, output.
8901
8940
  - One initiative per proposal \u2014 make multiple calls for multiple initiatives.
@@ -9407,71 +9446,59 @@ function makeProposeHandler(client) {
9407
9446
  }
9408
9447
  };
9409
9448
  }
9449
+ function createTool(definition, execute) {
9450
+ return { ...definition, execute };
9451
+ }
9410
9452
  function createOrchestratorTools(api) {
9411
9453
  const account = resolveAccountFromConfig(api);
9412
9454
  if (!account) return null;
9413
9455
  const client = new DeskFreeClient(account.botToken, account.apiUrl);
9414
9456
  return [
9415
- {
9416
- ...ORCHESTRATOR_TOOLS.STATE,
9417
- async execute(_id, _params) {
9418
- try {
9419
- const state = await client.getState();
9420
- return {
9421
- content: [{ type: "text", text: JSON.stringify(state, null, 2) }]
9422
- };
9423
- } catch (err) {
9424
- return errorResult(err);
9425
- }
9457
+ createTool(ORCHESTRATOR_TOOLS.STATE, async (_id, _params) => {
9458
+ try {
9459
+ const state = await client.getState();
9460
+ return {
9461
+ content: [{ type: "text", text: JSON.stringify(state, null, 2) }]
9462
+ };
9463
+ } catch (err) {
9464
+ return errorResult(err);
9426
9465
  }
9427
- },
9428
- {
9429
- ...ORCHESTRATOR_TOOLS.SCHEDULE_TASK,
9430
- async execute(_id, params) {
9431
- try {
9432
- const taskId = validateStringParam(params, "taskId", true);
9433
- const scheduledFor = params?.scheduledFor;
9434
- const reason = validateStringParam(params, "reason", true);
9435
- if (scheduledFor) {
9436
- const result = await client.snoozeTask({
9437
- taskId,
9438
- scheduledFor,
9439
- initiator: "human_request",
9440
- reason
9441
- });
9442
- return formatTaskResponse(
9443
- result,
9444
- `Task "${result.title}" scheduled for ${new Date(scheduledFor).toLocaleString()}`,
9445
- [
9446
- `Reason: ${reason}`,
9447
- "Task will resurface automatically at the scheduled time"
9448
- ]
9449
- );
9450
- } else {
9451
- const result = await client.unsnoozeTask({ taskId });
9452
- return formatTaskResponse(
9453
- result,
9454
- `Task "${result.title}" activated immediately`,
9455
- ["Task is now available in the active queue"]
9456
- );
9457
- }
9458
- } catch (err) {
9459
- return errorResult(err);
9466
+ }),
9467
+ createTool(ORCHESTRATOR_TOOLS.SCHEDULE_TASK, async (_id, params) => {
9468
+ try {
9469
+ const taskId = validateStringParam(params, "taskId", true);
9470
+ const scheduledFor = params?.scheduledFor;
9471
+ const reason = validateStringParam(params, "reason", true);
9472
+ if (scheduledFor) {
9473
+ const result = await client.snoozeTask({
9474
+ taskId,
9475
+ scheduledFor,
9476
+ initiator: "human_request",
9477
+ reason
9478
+ });
9479
+ return formatTaskResponse(
9480
+ result,
9481
+ `Task "${result.title}" scheduled for ${new Date(scheduledFor).toLocaleString()}`,
9482
+ [
9483
+ `Reason: ${reason}`,
9484
+ "Task will resurface automatically at the scheduled time"
9485
+ ]
9486
+ );
9487
+ } else {
9488
+ const result = await client.unsnoozeTask({ taskId });
9489
+ return formatTaskResponse(
9490
+ result,
9491
+ `Task "${result.title}" activated immediately`,
9492
+ ["Task is now available in the active queue"]
9493
+ );
9460
9494
  }
9495
+ } catch (err) {
9496
+ return errorResult(err);
9461
9497
  }
9462
- },
9463
- {
9464
- ...ORCHESTRATOR_TOOLS.REOPEN_TASK,
9465
- execute: makeReopenTaskHandler(client)
9466
- },
9467
- {
9468
- ...ORCHESTRATOR_TOOLS.SEND_MESSAGE,
9469
- execute: makeSendMessageHandler(client)
9470
- },
9471
- {
9472
- ...ORCHESTRATOR_TOOLS.PROPOSE,
9473
- execute: makeProposeHandler(client)
9474
- }
9498
+ }),
9499
+ createTool(ORCHESTRATOR_TOOLS.REOPEN_TASK, makeReopenTaskHandler(client)),
9500
+ createTool(ORCHESTRATOR_TOOLS.SEND_MESSAGE, makeSendMessageHandler(client)),
9501
+ createTool(ORCHESTRATOR_TOOLS.PROPOSE, makeProposeHandler(client))
9475
9502
  ];
9476
9503
  }
9477
9504
  function createWorkerTools(api) {
@@ -9480,113 +9507,95 @@ function createWorkerTools(api) {
9480
9507
  const client = new DeskFreeClient(account.botToken, account.apiUrl);
9481
9508
  const cachedSkillContext = /* @__PURE__ */ new Map();
9482
9509
  return [
9483
- {
9484
- ...WORKER_TOOLS.START_TASK,
9485
- async execute(_id, params) {
9486
- try {
9487
- const taskId = validateStringParam(params, "taskId", true);
9488
- const runnerId = validateStringParam(params, "runnerId", false);
9489
- const result = await client.claimTask({ taskId, runnerId });
9490
- setActiveTaskId(taskId);
9491
- let skillInstructions = "";
9492
- cachedSkillContext.clear();
9493
- if (result.skillContext?.length) {
9494
- for (const s of result.skillContext) {
9495
- cachedSkillContext.set(s.skillId, {
9496
- displayName: s.displayName,
9497
- instructions: s.instructions
9498
- });
9499
- }
9500
- skillInstructions = result.skillContext.map(
9501
- (s) => `
9510
+ createTool(WORKER_TOOLS.START_TASK, async (_id, params) => {
9511
+ try {
9512
+ const taskId = validateStringParam(params, "taskId", true);
9513
+ const runnerId = validateStringParam(params, "runnerId", false);
9514
+ const result = await client.claimTask({ taskId, runnerId });
9515
+ setActiveTaskId(taskId);
9516
+ let skillInstructions = "";
9517
+ cachedSkillContext.clear();
9518
+ if (result.skillContext?.length) {
9519
+ for (const s of result.skillContext) {
9520
+ cachedSkillContext.set(s.skillId, {
9521
+ displayName: s.displayName,
9522
+ instructions: s.instructions
9523
+ });
9524
+ }
9525
+ skillInstructions = result.skillContext.map(
9526
+ (s) => `
9502
9527
  \u26A0\uFE0F SKILL: ${s.displayName} (ID: ${s.skillId})
9503
9528
  ${s.criticalSection}`
9504
- ).join("\n\n---\n");
9505
- }
9506
- const trimmedTask = trimTaskContext(result);
9507
- const taskJson = JSON.stringify(
9508
- {
9509
- summary: `Claimed task "${result.title}" \u2014 full context loaded${result.skillContext?.length ? ` (${result.skillContext.length} skill${result.skillContext.length > 1 ? "s" : ""} loaded \u2014 use deskfree_read_skill_section for full details)` : ""}`,
9510
- mode: trimmedTask.mode ?? "work",
9511
- nextActions: [
9512
- "Read the instructions and message history carefully",
9513
- ...result.fileContext ? [
9514
- `Task has a linked file "${result.fileContext.name}" (ID: ${result.fileContext.fileId}) \u2014 use deskfree_update_file to save your work to it`
9515
- ] : [],
9516
- ...trimmedTask.mode === "evaluation" ? [
9517
- "This is an evaluation task \u2014 review the WoW context and provide your evaluation in deskfree_complete_task"
9518
- ] : [],
9519
- "Complete with deskfree_complete_task when done (summary required)"
9520
- ],
9521
- task: trimmedTask
9522
- },
9523
- null,
9524
- 2
9525
- );
9526
- const totalTokens = estimateTokens(skillInstructions) + estimateTokens(taskJson);
9527
- const budgetWarning = totalTokens > MAX_TASK_CONTEXT_TOKENS ? `
9529
+ ).join("\n\n---\n");
9530
+ }
9531
+ const trimmedTask = trimTaskContext(result);
9532
+ const taskJson = JSON.stringify(
9533
+ {
9534
+ summary: `Claimed task "${result.title}" \u2014 full context loaded${result.skillContext?.length ? ` (${result.skillContext.length} skill${result.skillContext.length > 1 ? "s" : ""} loaded \u2014 use deskfree_read_skill_section for full details)` : ""}`,
9535
+ mode: trimmedTask.mode ?? "work",
9536
+ nextActions: [
9537
+ "Read the instructions and message history carefully",
9538
+ ...result.fileContext ? [
9539
+ `Task has a linked file "${result.fileContext.name}" (ID: ${result.fileContext.fileId}) \u2014 use deskfree_update_file to save your work to it`
9540
+ ] : [],
9541
+ ...trimmedTask.mode === "evaluation" ? [
9542
+ "This is an evaluation task \u2014 review the WoW context and provide your evaluation in deskfree_complete_task"
9543
+ ] : [],
9544
+ "Complete with deskfree_complete_task when done (summary required)"
9545
+ ],
9546
+ task: trimmedTask
9547
+ },
9548
+ null,
9549
+ 2
9550
+ );
9551
+ const totalTokens = estimateTokens(skillInstructions) + estimateTokens(taskJson);
9552
+ const budgetWarning = totalTokens > MAX_TASK_CONTEXT_TOKENS ? `
9528
9553
  \u26A0\uFE0F Context budget: ~${totalTokens} tokens loaded (~${MAX_TASK_CONTEXT_TOKENS} target). Be concise in your reasoning.` : "";
9554
+ return {
9555
+ content: [
9556
+ ...skillInstructions ? [{ type: "text", text: skillInstructions }] : [],
9557
+ {
9558
+ type: "text",
9559
+ text: taskJson + budgetWarning
9560
+ }
9561
+ ]
9562
+ };
9563
+ } catch (err) {
9564
+ return errorResult(err);
9565
+ }
9566
+ }),
9567
+ createTool(WORKER_TOOLS.UPDATE_FILE, makeUpdateFileHandler(client)),
9568
+ createTool(WORKER_TOOLS.COMPLETE_TASK, makeCompleteTaskHandler(client)),
9569
+ createTool(WORKER_TOOLS.SEND_MESSAGE, makeSendMessageHandler(client)),
9570
+ createTool(WORKER_TOOLS.PROPOSE, makeProposeHandler(client)),
9571
+ createTool(WORKER_TOOLS.READ_SKILL, async (_id, params) => {
9572
+ try {
9573
+ const skillId = validateStringParam(params, "skillId", true);
9574
+ const cached = cachedSkillContext.get(skillId);
9575
+ if (!cached) {
9529
9576
  return {
9530
9577
  content: [
9531
- ...skillInstructions ? [{ type: "text", text: skillInstructions }] : [],
9532
9578
  {
9533
9579
  type: "text",
9534
- text: taskJson + budgetWarning
9580
+ text: `Skill ${skillId} not found in current task context. Available: ${[...cachedSkillContext.keys()].join(", ") || "none"}`
9535
9581
  }
9536
9582
  ]
9537
9583
  };
9538
- } catch (err) {
9539
- return errorResult(err);
9540
9584
  }
9541
- }
9542
- },
9543
- {
9544
- ...WORKER_TOOLS.UPDATE_FILE,
9545
- execute: makeUpdateFileHandler(client)
9546
- },
9547
- {
9548
- ...WORKER_TOOLS.COMPLETE_TASK,
9549
- execute: makeCompleteTaskHandler(client)
9550
- },
9551
- {
9552
- ...WORKER_TOOLS.SEND_MESSAGE,
9553
- execute: makeSendMessageHandler(client)
9554
- },
9555
- {
9556
- ...WORKER_TOOLS.PROPOSE,
9557
- execute: makeProposeHandler(client)
9558
- },
9559
- {
9560
- ...WORKER_TOOLS.READ_SKILL,
9561
- async execute(_id, params) {
9562
- try {
9563
- const skillId = validateStringParam(params, "skillId", true);
9564
- const cached = cachedSkillContext.get(skillId);
9565
- if (!cached) {
9566
- return {
9567
- content: [
9568
- {
9569
- type: "text",
9570
- text: `Skill ${skillId} not found in current task context. Available: ${[...cachedSkillContext.keys()].join(", ") || "none"}`
9571
- }
9572
- ]
9573
- };
9574
- }
9575
- return {
9576
- content: [
9577
- {
9578
- type: "text",
9579
- text: `## ${cached.displayName} \u2014 Full Instructions
9585
+ return {
9586
+ content: [
9587
+ {
9588
+ type: "text",
9589
+ text: `## ${cached.displayName} \u2014 Full Instructions
9580
9590
 
9581
9591
  ${cached.instructions}`
9582
- }
9583
- ]
9584
- };
9585
- } catch (err) {
9586
- return errorResult(err);
9587
- }
9592
+ }
9593
+ ]
9594
+ };
9595
+ } catch (err) {
9596
+ return errorResult(err);
9588
9597
  }
9589
- }
9598
+ })
9590
9599
  ];
9591
9600
  }
9592
9601