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

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,138 @@ 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
+ async function pollAndDeliver(client, ctx, cursor, log, account, alreadyWelcomed) {
4999
+ try {
5000
+ const isFirstRun = !cursor;
5001
+ const response = await client.listMessages({
5002
+ ...cursor ? { cursor } : {}
5003
+ });
5004
+ if (isFirstRun) {
5005
+ const seedCursor = response.cursor ?? (/* @__PURE__ */ new Date()).toISOString();
5006
+ if (response.cursor) {
5007
+ log.info(
5008
+ `First run: skipping ${response.items.length} existing message(s), seeding cursor.`
5009
+ );
5010
+ } else {
5011
+ log.info(
5012
+ "First run: no messages yet (empty inbox), seeding cursor with current time."
5013
+ );
5014
+ }
5015
+ saveCursor(ctx, seedCursor, log);
5016
+ log.info("Connected to DeskFree. Ready to receive messages and tasks.");
5017
+ if (alreadyWelcomed) {
5018
+ log.info("Welcome already sent, skipping duplicate.");
5019
+ return {
5020
+ cursor: seedCursor,
5021
+ ok: true,
5022
+ welcomeSent: false
5023
+ };
5024
+ }
5025
+ try {
5026
+ const botName = account?.botName;
5027
+ const humanName = account?.humanName;
5028
+ 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.)";
5029
+ const welcomeMessage = {
5030
+ messageId: `welcome-${Date.now()}`,
5031
+ botId: "",
5032
+ humanId: account?.userId ?? ctx.account.userId,
5033
+ authorType: "user",
5034
+ content: welcomeContent,
5035
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5036
+ userName: humanName ?? "System"
5037
+ };
5038
+ await deliverMessageToAgent(ctx, welcomeMessage, client);
5039
+ log.info("Sent welcome message to agent.");
5040
+ } catch (err) {
5041
+ const msg = err instanceof Error ? err.message : String(err);
5042
+ log.warn(`Failed to send welcome message: ${msg}`);
5043
+ }
5044
+ return { cursor: seedCursor, ok: true, welcomeSent: true };
5045
+ }
5046
+ if (response.items.length === 0) return { cursor: null, ok: true };
5047
+ const newItems = response.items.filter(
5048
+ (m) => !deliveredMessageIds.has(m.messageId)
5049
+ );
5050
+ if (newItems.length === 0) {
5051
+ log.debug(
5052
+ `Poll returned ${response.items.length} item(s), all already delivered.`
5053
+ );
5054
+ if (response.cursor) {
5055
+ saveCursor(ctx, response.cursor, log);
5056
+ }
5057
+ return { cursor: response.cursor, ok: true };
5058
+ }
5059
+ log.info(
5060
+ `Received ${newItems.length} new message(s) (${response.items.length - newItems.length} duplicate(s) skipped).`
5061
+ );
5062
+ let deliveredCount = 0;
5063
+ for (const message of newItems) {
5064
+ if (message.authorType === "bot") {
5065
+ log.debug(`Skipping bot message ${message.messageId}`);
5066
+ deliveredMessageIds.add(message.messageId);
5067
+ continue;
5068
+ }
5069
+ clearCompletedTaskId();
5070
+ setInboundThreadId(message.taskId ?? null);
5071
+ await deliverMessageToAgent(ctx, message, client);
5072
+ deliveredMessageIds.add(message.messageId);
5073
+ deliveredCount++;
5074
+ sendWsAck(message.messageId);
5075
+ }
5076
+ if (deliveredMessageIds.size > 1e3) {
5077
+ const entries = Array.from(deliveredMessageIds);
5078
+ deliveredMessageIds.clear();
5079
+ for (const id of entries.slice(-500)) {
5080
+ deliveredMessageIds.add(id);
5081
+ }
5082
+ }
5083
+ if (deliveredCount > 0) {
5084
+ recordMessageDelivery(ctx.accountId, deliveredCount);
5085
+ }
5086
+ if (response.cursor) {
5087
+ saveCursor(ctx, response.cursor, log);
5088
+ }
5089
+ return { cursor: response.cursor, ok: true };
5090
+ } catch (err) {
5091
+ const message = err instanceof Error ? err.message : String(err);
5092
+ log.warn(`Poll failed: ${message}`);
5093
+ reportError("warn", `Message poll failed: ${message}`, {
5094
+ component: "gateway",
5095
+ event: "poll_failed"
5096
+ });
5097
+ return { cursor: null, ok: false };
5098
+ }
5099
+ }
5100
+
5101
+ // src/version.ts
5102
+ import { readFileSync as readFileSync2 } from "fs";
5103
+ import { resolve } from "path";
5104
+ var PLUGIN_VERSION = JSON.parse(
5105
+ readFileSync2(resolve(__dirname, "..", "package.json"), "utf-8")
5106
+ ).version;
5107
+
5108
+ // src/gateway.ts
5109
+ var PING_INTERVAL_MS = 5 * 60 * 1e3;
5110
+ var POLL_FALLBACK_INTERVAL_MS = 30 * 1e3;
5111
+ var WS_CONNECTION_TIMEOUT_MS = 30 * 1e3;
5112
+ var WS_PONG_TIMEOUT_MS = 10 * 1e3;
5113
+ var NOTIFY_DEBOUNCE_MS = 200;
5114
+ var BACKOFF_INITIAL_MS = 2e3;
5115
+ var BACKOFF_MAX_MS = 3e4;
5116
+ var BACKOFF_FACTOR = 1.8;
5117
+ var HEALTH_LOG_INTERVAL_MS = 30 * 60 * 1e3;
5118
+ var MAX_CONSECUTIVE_POLL_FAILURES = 5;
4859
5119
  function nextBackoff(state) {
4860
5120
  const delay = Math.min(
4861
5121
  BACKOFF_INITIAL_MS * Math.pow(BACKOFF_FACTOR, state.attempt),
@@ -4881,32 +5141,12 @@ function sleepWithAbort(ms, signal) {
4881
5141
  signal.addEventListener("abort", onAbort, { once: true });
4882
5142
  });
4883
5143
  }
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
5144
  async function startDeskFreeConnection(ctx) {
4906
5145
  const account = ctx.account;
4907
5146
  const client = new DeskFreeClient(account.botToken, account.apiUrl);
4908
5147
  const log = ctx.log ?? getDeskFreeRuntime().logging.createLogger("deskfree");
4909
5148
  let cursor = loadCursor(ctx);
5149
+ let welcomeSent = false;
4910
5150
  const backoff = { attempt: 0 };
4911
5151
  let totalReconnects = 0;
4912
5152
  initializeHealth(ctx.accountId);
@@ -4946,7 +5186,11 @@ async function startDeskFreeConnection(ctx) {
4946
5186
  ctx,
4947
5187
  cursor,
4948
5188
  log,
4949
- account
5189
+ account,
5190
+ getWelcomeSent: () => welcomeSent,
5191
+ setWelcomeSent: (v) => {
5192
+ welcomeSent = v;
5193
+ }
4950
5194
  });
4951
5195
  totalReconnects++;
4952
5196
  } catch (err) {
@@ -4988,7 +5232,16 @@ async function startDeskFreeConnection(ctx) {
4988
5232
  );
4989
5233
  }
4990
5234
  async function runWebSocketConnection(opts) {
4991
- const { ticket, wsUrl, client, ctx, log, account } = opts;
5235
+ const {
5236
+ ticket,
5237
+ wsUrl,
5238
+ client,
5239
+ ctx,
5240
+ log,
5241
+ account,
5242
+ getWelcomeSent,
5243
+ setWelcomeSent
5244
+ } = opts;
4992
5245
  let cursor = opts.cursor;
4993
5246
  return new Promise((resolve2, reject) => {
4994
5247
  const ws = new wrapper_default(`${wsUrl}?ticket=${ticket}`);
@@ -5031,7 +5284,7 @@ async function runWebSocketConnection(opts) {
5031
5284
  }, WS_CONNECTION_TIMEOUT_MS);
5032
5285
  ws.on("open", async () => {
5033
5286
  isConnected = true;
5034
- activeWs = ws;
5287
+ setActiveWs(ws);
5035
5288
  if (connectionTimer !== void 0) {
5036
5289
  clearTimeout(connectionTimer);
5037
5290
  connectionTimer = void 0;
@@ -5077,7 +5330,9 @@ async function runWebSocketConnection(opts) {
5077
5330
  cursor = c ?? cursor;
5078
5331
  },
5079
5332
  log,
5080
- account
5333
+ account,
5334
+ getWelcomeSent,
5335
+ setWelcomeSent
5081
5336
  );
5082
5337
  client.listMessages({ limit: 20 }).then((res) => {
5083
5338
  const humanMessages = res.items?.filter(
@@ -5094,9 +5349,11 @@ async function runWebSocketConnection(opts) {
5094
5349
  try {
5095
5350
  const raw = data.toString();
5096
5351
  if (!raw || raw.length > 65536) {
5097
- log.warn(
5098
- `Ignoring oversized or empty WS message (${raw?.length ?? 0} bytes)`
5099
- );
5352
+ if (raw && raw.length > 65536) {
5353
+ log.warn(`Ignoring oversized WS message (${raw.length} bytes)`);
5354
+ } else {
5355
+ log.debug(`Ignoring empty WS frame (${raw?.length ?? 0} bytes)`);
5356
+ }
5100
5357
  return;
5101
5358
  }
5102
5359
  const msg = JSON.parse(raw);
@@ -5117,7 +5374,10 @@ async function runWebSocketConnection(opts) {
5117
5374
  (c) => {
5118
5375
  cursor = c ?? cursor;
5119
5376
  },
5120
- log
5377
+ log,
5378
+ account,
5379
+ getWelcomeSent,
5380
+ setWelcomeSent
5121
5381
  );
5122
5382
  }, NOTIFY_DEBOUNCE_MS);
5123
5383
  } else if (msg.action === "pong") {
@@ -5135,7 +5395,7 @@ async function runWebSocketConnection(opts) {
5135
5395
  ws.on("close", (code, reason) => {
5136
5396
  cleanup();
5137
5397
  isConnected = false;
5138
- if (activeWs === ws) activeWs = null;
5398
+ if (activeWs === ws) setActiveWs(null);
5139
5399
  ctx.setStatus({ running: false, lastStopAt: Date.now() });
5140
5400
  if (code === 1e3) {
5141
5401
  log.info(`WebSocket closed normally: ${code} ${reason.toString()}`);
@@ -5185,7 +5445,7 @@ async function runWebSocketConnection(opts) {
5185
5445
  ws.on("error", (err) => {
5186
5446
  cleanup();
5187
5447
  isConnected = false;
5188
- if (activeWs === ws) activeWs = null;
5448
+ if (activeWs === ws) setActiveWs(null);
5189
5449
  const errorMessage = err instanceof Error ? err.message : String(err);
5190
5450
  log.error(`WebSocket error: ${errorMessage}`);
5191
5451
  reportError("error", `WebSocket error: ${errorMessage}`, {
@@ -5209,144 +5469,51 @@ async function runWebSocketConnection(opts) {
5209
5469
  ws.close(1e3, "shutdown");
5210
5470
  }
5211
5471
  } 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).`
5472
+ const message = err instanceof Error ? err.message : String(err);
5473
+ log.warn(`Error closing WebSocket during shutdown: ${message}`);
5474
+ }
5475
+ },
5476
+ { once: true }
5312
5477
  );
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);
5478
+ });
5479
+ }
5480
+ async function runPollingFallback(opts) {
5481
+ const { client, ctx, log } = opts;
5482
+ let cursor = opts.cursor;
5483
+ let iterations = 0;
5484
+ const maxIterations = 10;
5485
+ ctx.setStatus({ running: true, lastStartAt: Date.now() });
5486
+ log.info("Running in polling fallback mode.");
5487
+ let consecutiveFailures = 0;
5488
+ while (!ctx.abortSignal.aborted && iterations < maxIterations) {
5489
+ const result = await pollAndDeliver(client, ctx, cursor, log);
5490
+ if (result.ok) {
5491
+ if (result.cursor) cursor = result.cursor;
5492
+ consecutiveFailures = 0;
5493
+ } else {
5494
+ consecutiveFailures++;
5495
+ if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
5496
+ log.warn(
5497
+ `${consecutiveFailures} consecutive poll failures, breaking out to retry WebSocket`
5498
+ );
5499
+ reportError(
5500
+ "error",
5501
+ `${consecutiveFailures} consecutive poll failures, switching to WebSocket retry`,
5502
+ {
5503
+ component: "gateway",
5504
+ event: "poll_max_failures",
5505
+ consecutiveFailures
5506
+ }
5507
+ );
5508
+ break;
5332
5509
  }
5333
5510
  }
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 };
5511
+ iterations++;
5512
+ const jitter = Math.random() * POLL_FALLBACK_INTERVAL_MS * 0.2;
5513
+ await sleepWithAbort(POLL_FALLBACK_INTERVAL_MS + jitter, ctx.abortSignal);
5349
5514
  }
5515
+ ctx.setStatus({ running: false, lastStopAt: Date.now() });
5516
+ return cursor;
5350
5517
  }
5351
5518
 
5352
5519
  // node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
@@ -8042,7 +8209,7 @@ var ORCHESTRATOR_TOOLS = {
8042
8209
  }),
8043
8210
  instructions: Type.Optional(
8044
8211
  Type.String({
8045
- description: "Concise instructions in simple markdown (bold, lists, inline code \u2014 no # headers). Brief a contractor: what to do, what done looks like. Skip obvious context."
8212
+ description: "3-8 bullet points max in simple markdown (bold, lists, inline code \u2014 no # headers). What to do, what done looks like, key constraints. Under 500 words. Skip context the worker can infer from the title."
8046
8213
  })
8047
8214
  ),
8048
8215
  file: Type.Optional(
@@ -8129,11 +8296,11 @@ var SHARED_TOOLS = {
8129
8296
  Type.Object(
8130
8297
  {
8131
8298
  reasoning: Type.String({
8132
- description: "Why these updates matter \u2014 what was learned from this task"
8299
+ description: "1-2 sentences: what changed and why. No restating known context."
8133
8300
  }),
8134
8301
  globalWoW: Type.Optional(
8135
8302
  Type.String({
8136
- description: "Full updated global Ways of Working markdown content (full replacement, not diff)"
8303
+ description: "Updated global Ways of Working (full replacement). Keep surgical \u2014 only add or modify relevant sections, do not inflate with restated context."
8137
8304
  })
8138
8305
  ),
8139
8306
  initiativeId: Type.Optional(
@@ -8143,7 +8310,7 @@ var SHARED_TOOLS = {
8143
8310
  ),
8144
8311
  initiativeContent: Type.Optional(
8145
8312
  Type.String({
8146
- description: "Full updated initiative content markdown (full replacement, not diff)"
8313
+ description: "Updated initiative content (full replacement). Keep surgical \u2014 evolve what exists, do not rewrite from scratch."
8147
8314
  })
8148
8315
  )
8149
8316
  },
@@ -8160,7 +8327,7 @@ var SHARED_TOOLS = {
8160
8327
  }),
8161
8328
  instructions: Type.Optional(
8162
8329
  Type.String({
8163
- description: "Instructions for the follow-up task"
8330
+ description: "Brief handoff: 2-3 bullet points max. The next worker gets parent task context."
8164
8331
  })
8165
8332
  )
8166
8333
  }),
@@ -8230,7 +8397,7 @@ var SHARED_TOOLS = {
8230
8397
  }),
8231
8398
  instructions: Type.Optional(
8232
8399
  Type.String({
8233
- description: "Concise instructions in simple markdown (bold, lists, inline code \u2014 no # headers). Brief a contractor: what to do, what done looks like. Skip obvious context."
8400
+ description: "3-8 bullet points max in simple markdown (bold, lists, inline code \u2014 no # headers). What to do, what done looks like, key constraints. Under 500 words. Skip context the worker can infer from the title."
8234
8401
  })
8235
8402
  ),
8236
8403
  file: Type.Optional(
@@ -8349,131 +8516,6 @@ var STATUS_MESSAGES = {
8349
8516
  function getChannelConfig(cfg) {
8350
8517
  return cfg?.channels?.deskfree ?? null;
8351
8518
  }
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
8519
  var deskFreePlugin = {
8478
8520
  id: "deskfree",
8479
8521
  meta: CHANNEL_META,
@@ -8894,9 +8936,9 @@ You are the orchestrator. Your job: turn human intent into approved tasks, then
8894
8936
 
8895
8937
  **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
8938
 
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.
8899
- - Write task instructions as rich markdown (bold, lists, inline code \u2014 no # headers). Brief a contractor who has never seen the codebase.
8939
+ 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.
8940
+ - 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.
8941
+ - Write task instructions as 3-8 bullet points max (bold, lists, inline code \u2014 no # headers). What to do, what done looks like, key constraints. Under 500 words \u2014 brief a contractor, not write a spec.
8900
8942
  - Estimate token cost per task \u2014 consider files to read, reasoning, output.
8901
8943
  - One initiative per proposal \u2014 make multiple calls for multiple initiatives.
8902
8944
  - Initiative titles should reflect aspirations and outcomes, not activities. "AI Thought Leadership on LinkedIn" over "LinkedIn Content."
@@ -8905,10 +8947,10 @@ var DESKFREE_WORKER_DIRECTIVE = `## DeskFree Worker
8905
8947
  You are a worker sub-agent. Call \`deskfree_start_task\` with your taskId to claim and load context.
8906
8948
  Tools: deskfree_start_task, deskfree_update_file, deskfree_complete_task, deskfree_send_message, deskfree_propose.
8907
8949
  - Claim your task first with deskfree_start_task \u2014 this loads instructions, messages, and file context.
8908
- - Save work to linked files with deskfree_update_file (incrementally).
8950
+ - Save work to linked files with deskfree_update_file. Build up incrementally \u2014 start with structure/outline, then flesh out. Send an "ask" for review before going deep. The human should see the shape before the details.
8909
8951
  - Use deskfree_send_message with type "notify" for progress updates \u2014 what you're doing, what you found. Keep it brief.
8910
8952
  - Use deskfree_send_message with type "ask" when you need human input OR when your work is done for review. This surfaces to the main thread. Terminate after sending an ask.
8911
- - When completing: pass "learnings" to deskfree_complete_task if you have knowledge updates (WoW or initiative content). Pass "followUps" if the work revealed clear next steps. Knowledge updates and proposals happen atomically with completion \u2014 no separate tool calls needed.
8953
+ - When completing: pass "learnings" only if you have genuine new knowledge. WoW updates should be surgical \u2014 add or modify only the relevant section, do not restate existing content. Reasoning should be 1-2 sentences. Pass "followUps" as brief handoffs (title + 2-3 bullets each).
8912
8954
  - Only complete when the human has confirmed or no review is needed.
8913
8955
  - Write like a senior colleague giving a status update \u2014 not a report. 1-3 sentences for messages.
8914
8956
  - On 409 or 404 errors: STOP. Do not retry the same taskId. Call deskfree_state to find available tasks.`;
@@ -9407,71 +9449,59 @@ function makeProposeHandler(client) {
9407
9449
  }
9408
9450
  };
9409
9451
  }
9452
+ function createTool(definition, execute) {
9453
+ return { ...definition, execute };
9454
+ }
9410
9455
  function createOrchestratorTools(api) {
9411
9456
  const account = resolveAccountFromConfig(api);
9412
9457
  if (!account) return null;
9413
9458
  const client = new DeskFreeClient(account.botToken, account.apiUrl);
9414
9459
  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
- }
9460
+ createTool(ORCHESTRATOR_TOOLS.STATE, async (_id, _params) => {
9461
+ try {
9462
+ const state = await client.getState();
9463
+ return {
9464
+ content: [{ type: "text", text: JSON.stringify(state, null, 2) }]
9465
+ };
9466
+ } catch (err) {
9467
+ return errorResult(err);
9426
9468
  }
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);
9469
+ }),
9470
+ createTool(ORCHESTRATOR_TOOLS.SCHEDULE_TASK, async (_id, params) => {
9471
+ try {
9472
+ const taskId = validateStringParam(params, "taskId", true);
9473
+ const scheduledFor = params?.scheduledFor;
9474
+ const reason = validateStringParam(params, "reason", true);
9475
+ if (scheduledFor) {
9476
+ const result = await client.snoozeTask({
9477
+ taskId,
9478
+ scheduledFor,
9479
+ initiator: "human_request",
9480
+ reason
9481
+ });
9482
+ return formatTaskResponse(
9483
+ result,
9484
+ `Task "${result.title}" scheduled for ${new Date(scheduledFor).toLocaleString()}`,
9485
+ [
9486
+ `Reason: ${reason}`,
9487
+ "Task will resurface automatically at the scheduled time"
9488
+ ]
9489
+ );
9490
+ } else {
9491
+ const result = await client.unsnoozeTask({ taskId });
9492
+ return formatTaskResponse(
9493
+ result,
9494
+ `Task "${result.title}" activated immediately`,
9495
+ ["Task is now available in the active queue"]
9496
+ );
9460
9497
  }
9498
+ } catch (err) {
9499
+ return errorResult(err);
9461
9500
  }
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
- }
9501
+ }),
9502
+ createTool(ORCHESTRATOR_TOOLS.REOPEN_TASK, makeReopenTaskHandler(client)),
9503
+ createTool(ORCHESTRATOR_TOOLS.SEND_MESSAGE, makeSendMessageHandler(client)),
9504
+ createTool(ORCHESTRATOR_TOOLS.PROPOSE, makeProposeHandler(client))
9475
9505
  ];
9476
9506
  }
9477
9507
  function createWorkerTools(api) {
@@ -9480,113 +9510,95 @@ function createWorkerTools(api) {
9480
9510
  const client = new DeskFreeClient(account.botToken, account.apiUrl);
9481
9511
  const cachedSkillContext = /* @__PURE__ */ new Map();
9482
9512
  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) => `
9513
+ createTool(WORKER_TOOLS.START_TASK, async (_id, params) => {
9514
+ try {
9515
+ const taskId = validateStringParam(params, "taskId", true);
9516
+ const runnerId = validateStringParam(params, "runnerId", false);
9517
+ const result = await client.claimTask({ taskId, runnerId });
9518
+ setActiveTaskId(taskId);
9519
+ let skillInstructions = "";
9520
+ cachedSkillContext.clear();
9521
+ if (result.skillContext?.length) {
9522
+ for (const s of result.skillContext) {
9523
+ cachedSkillContext.set(s.skillId, {
9524
+ displayName: s.displayName,
9525
+ instructions: s.instructions
9526
+ });
9527
+ }
9528
+ skillInstructions = result.skillContext.map(
9529
+ (s) => `
9502
9530
  \u26A0\uFE0F SKILL: ${s.displayName} (ID: ${s.skillId})
9503
9531
  ${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 ? `
9532
+ ).join("\n\n---\n");
9533
+ }
9534
+ const trimmedTask = trimTaskContext(result);
9535
+ const taskJson = JSON.stringify(
9536
+ {
9537
+ 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)` : ""}`,
9538
+ mode: trimmedTask.mode ?? "work",
9539
+ nextActions: [
9540
+ "Read the instructions and message history carefully",
9541
+ ...result.fileContext ? [
9542
+ `Task has a linked file "${result.fileContext.name}" (ID: ${result.fileContext.fileId}) \u2014 use deskfree_update_file to save your work to it`
9543
+ ] : [],
9544
+ ...trimmedTask.mode === "evaluation" ? [
9545
+ "This is an evaluation task \u2014 review the WoW context and provide your evaluation in deskfree_complete_task"
9546
+ ] : [],
9547
+ "Complete with deskfree_complete_task when done (summary required)"
9548
+ ],
9549
+ task: trimmedTask
9550
+ },
9551
+ null,
9552
+ 2
9553
+ );
9554
+ const totalTokens = estimateTokens(skillInstructions) + estimateTokens(taskJson);
9555
+ const budgetWarning = totalTokens > MAX_TASK_CONTEXT_TOKENS ? `
9528
9556
  \u26A0\uFE0F Context budget: ~${totalTokens} tokens loaded (~${MAX_TASK_CONTEXT_TOKENS} target). Be concise in your reasoning.` : "";
9557
+ return {
9558
+ content: [
9559
+ ...skillInstructions ? [{ type: "text", text: skillInstructions }] : [],
9560
+ {
9561
+ type: "text",
9562
+ text: taskJson + budgetWarning
9563
+ }
9564
+ ]
9565
+ };
9566
+ } catch (err) {
9567
+ return errorResult(err);
9568
+ }
9569
+ }),
9570
+ createTool(WORKER_TOOLS.UPDATE_FILE, makeUpdateFileHandler(client)),
9571
+ createTool(WORKER_TOOLS.COMPLETE_TASK, makeCompleteTaskHandler(client)),
9572
+ createTool(WORKER_TOOLS.SEND_MESSAGE, makeSendMessageHandler(client)),
9573
+ createTool(WORKER_TOOLS.PROPOSE, makeProposeHandler(client)),
9574
+ createTool(WORKER_TOOLS.READ_SKILL, async (_id, params) => {
9575
+ try {
9576
+ const skillId = validateStringParam(params, "skillId", true);
9577
+ const cached = cachedSkillContext.get(skillId);
9578
+ if (!cached) {
9529
9579
  return {
9530
9580
  content: [
9531
- ...skillInstructions ? [{ type: "text", text: skillInstructions }] : [],
9532
9581
  {
9533
9582
  type: "text",
9534
- text: taskJson + budgetWarning
9583
+ text: `Skill ${skillId} not found in current task context. Available: ${[...cachedSkillContext.keys()].join(", ") || "none"}`
9535
9584
  }
9536
9585
  ]
9537
9586
  };
9538
- } catch (err) {
9539
- return errorResult(err);
9540
9587
  }
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
9588
+ return {
9589
+ content: [
9590
+ {
9591
+ type: "text",
9592
+ text: `## ${cached.displayName} \u2014 Full Instructions
9580
9593
 
9581
9594
  ${cached.instructions}`
9582
- }
9583
- ]
9584
- };
9585
- } catch (err) {
9586
- return errorResult(err);
9587
- }
9595
+ }
9596
+ ]
9597
+ };
9598
+ } catch (err) {
9599
+ return errorResult(err);
9588
9600
  }
9589
- }
9601
+ })
9590
9602
  ];
9591
9603
  }
9592
9604