@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 +549 -537
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/skills/deskfree/SKILL.md +2 -3
- package/skills/deskfree/references/tools.md +12 -1
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(
|
|
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
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
5098
|
-
`Ignoring oversized
|
|
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)
|
|
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)
|
|
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
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
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
|
-
|
|
5335
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
8899
|
-
- Write task instructions as
|
|
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
|
|
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"
|
|
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
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
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
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
const
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
`
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
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
|
-
|
|
9465
|
-
|
|
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
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
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
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
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:
|
|
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
|
-
|
|
9545
|
-
|
|
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
|
-
|
|
9586
|
-
|
|
9587
|
-
}
|
|
9595
|
+
}
|
|
9596
|
+
]
|
|
9597
|
+
};
|
|
9598
|
+
} catch (err) {
|
|
9599
|
+
return errorResult(err);
|
|
9588
9600
|
}
|
|
9589
|
-
}
|
|
9601
|
+
})
|
|
9590
9602
|
];
|
|
9591
9603
|
}
|
|
9592
9604
|
|